@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,946 @@
1
+ import runtimeSafety from "./hellolynk-runtime-safety.cjs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { normalizeHelloLynkBaseUrl } from "./hellolynk-client-core.mjs";
4
+
5
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/;
6
+ const MAX_SSE_BUFFER_BYTES = 256 * 1024;
7
+ const DEFAULT_SESSION_ROTATION_LEAD_MS = 30_000;
8
+ const DEFAULT_JSON_REQUEST_TIMEOUT_MS = 8_000;
9
+ const MAX_JSON_REQUEST_TIMEOUT_MS = 120_000;
10
+ // The relay sends byte-level keepalives every 20 seconds by default.
11
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 90_000;
12
+ const MAX_STREAM_IDLE_TIMEOUT_MS = 300_000;
13
+
14
+ export class HelloLynkHostTransportClientError extends Error {
15
+ constructor(code, options = {}) {
16
+ super(code, options.cause ? { cause: options.cause } : undefined);
17
+ this.name = "HelloLynkHostTransportClientError";
18
+ this.code = code;
19
+ this.httpStatus = Number.isInteger(options.httpStatus) ? options.httpStatus : null;
20
+ }
21
+ }
22
+
23
+ export function createHelloLynkHostTransportClient(options = {}) {
24
+ const baseUrl = normalizeHelloLynkBaseUrl(options.baseUrl, {
25
+ allowLoopbackHttp: options.allowLoopbackHttp === true,
26
+ }).replace(/\/api\/v1$/, "");
27
+ const fetchImpl = options.fetchImpl || runtimeSafety.helloLynkNetworkFetch;
28
+ const credentialProvider = options.credentialProvider;
29
+ const sessionProvider = options.sessionProvider;
30
+ const runtimeHeadersForDelegate = options.runtimeHeadersForDelegate;
31
+ const jsonRequestTimeoutMs = positiveInteger(
32
+ options.requestTimeoutMs ?? DEFAULT_JSON_REQUEST_TIMEOUT_MS,
33
+ "host_transport_request_timeout_invalid",
34
+ );
35
+ if (jsonRequestTimeoutMs > MAX_JSON_REQUEST_TIMEOUT_MS) {
36
+ throw failure("host_transport_request_timeout_invalid");
37
+ }
38
+ const streamIdleTimeoutMs = positiveInteger(
39
+ options.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS,
40
+ "host_transport_stream_idle_timeout_invalid",
41
+ );
42
+ if (streamIdleTimeoutMs > MAX_STREAM_IDLE_TIMEOUT_MS) {
43
+ throw failure("host_transport_stream_idle_timeout_invalid");
44
+ }
45
+ const owner = Object.freeze({
46
+ principalId: safeId(options.expectedPrincipalId, "host_transport_principal_invalid"),
47
+ hostId: safeId(options.expectedHostId, "host_transport_host_invalid"),
48
+ });
49
+ const registeredEndpoints = new Map();
50
+ const lifetime = new AbortController();
51
+ // Standalone transports keep their own identity. The process-owning Host
52
+ // supplies a durable attempt only after taking its installation lock.
53
+ const receiverId = randomUUID();
54
+ let receiverSequence = 0;
55
+ let activeStream = null;
56
+ let activeSessionPending = null;
57
+ let issuedSessionPending = null;
58
+ let sessionPersistencePending = null;
59
+ let cachedSession = null;
60
+ let latestSessionId = null;
61
+ let staleSessionId = null;
62
+ let rotateBeforeNextStreamSessionId = null;
63
+ let sessionGeneration = 0;
64
+ if (typeof fetchImpl !== "function") throw failure("host_transport_fetch_unavailable");
65
+ if (!credentialProvider || typeof credentialProvider.get !== "function") {
66
+ throw failure("host_transport_credential_provider_required");
67
+ }
68
+ if (!sessionProvider || typeof sessionProvider.get !== "function"
69
+ || typeof sessionProvider.save !== "function") {
70
+ throw failure("host_transport_session_provider_required");
71
+ }
72
+
73
+ async function issueSession(signal) {
74
+ assertUsable(signal);
75
+ if (!issuedSessionPending) {
76
+ cachedSession = null;
77
+ sessionGeneration += 1;
78
+ const pending = mintSession();
79
+ issuedSessionPending = pending;
80
+ pending.finally(() => {
81
+ if (issuedSessionPending === pending) issuedSessionPending = null;
82
+ }).catch(() => {});
83
+ }
84
+ return waitForSession(issuedSessionPending, signal);
85
+ }
86
+
87
+ async function mintSession() {
88
+ const credential = String(await credentialProvider.get() || "");
89
+ assertUsable();
90
+ if (!credential) throw failure("host_transport_credential_required");
91
+ const result = await jsonRequest("/api/v2/host-transport/sessions", {
92
+ method: "POST",
93
+ headers: { "x-hellolynk-host-transport-credential": credential },
94
+ body: {},
95
+ });
96
+ const session = validateSession({
97
+ sessionId: safeId(result.hostTransportSessionId, "host_transport_session_invalid"),
98
+ sessionSecret: String(result.hostTransportSessionSecret || ""),
99
+ principalId: safeId(result.principalId, "host_transport_principal_invalid"),
100
+ hostId: safeId(result.hostId, "host_transport_host_invalid"),
101
+ expiresAt: String(result.expiresAt || ""),
102
+ }, owner);
103
+ // A stopped transport must not publish a late cloud response into the
104
+ // protected slot now used by the replacement Host.
105
+ assertUsable();
106
+ const persistence = Promise.resolve(sessionProvider.save(session));
107
+ sessionPersistencePending = persistence;
108
+ try { await persistence; }
109
+ finally { if (sessionPersistencePending === persistence) sessionPersistencePending = null; }
110
+ assertUsable();
111
+ cachedSession = session;
112
+ latestSessionId = session.sessionId;
113
+ staleSessionId = null;
114
+ return session;
115
+ }
116
+
117
+ async function registerEndpoint(input = {}) {
118
+ assertUsable(input.signal);
119
+ const session = await activeSession(input.signal);
120
+ assertUsable(input.signal);
121
+ const runtimeHeaders = await requiredRuntimeHeaders(input.runtimeHeaders);
122
+ assertUsable(input.signal);
123
+ const result = await jsonRequest("/api/v2/host-transport/endpoints", {
124
+ method: "POST",
125
+ headers: {
126
+ ...hostSessionHeaders(session),
127
+ ...runtimeHeaders,
128
+ },
129
+ body: { identityKeyId: safeId(input.identityKeyId, "host_transport_identity_invalid") },
130
+ signal: input.signal,
131
+ });
132
+ input.signal?.throwIfAborted();
133
+ const endpoint = validateEndpointResult(result, owner);
134
+ if (input.delegateId != null && endpoint.delegateId !== input.delegateId) {
135
+ throw failure("host_transport_endpoint_response_mismatch");
136
+ }
137
+ registeredEndpoints.set(endpointRouteKey(endpoint.deviceId, endpoint.identityKeyId), endpoint);
138
+ return result;
139
+ }
140
+
141
+ async function openEventStream(input = {}) {
142
+ assertUsable(input.signal);
143
+ if (activeStream) throw failure("host_transport_stream_already_open");
144
+ const slot = {};
145
+ activeStream = slot;
146
+ try {
147
+ return await openOwnedEventStream(input, slot);
148
+ } catch (error) {
149
+ if (activeStream === slot) activeStream = null;
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ async function openOwnedEventStream(input, slot) {
155
+ let attempt;
156
+ if (input.receiverAttempt !== undefined) {
157
+ requireExactFields(input.receiverAttempt, ["receiverId", "sequence"], "host_transport_receiver_invalid");
158
+ attempt = { receiverId: safeId(input.receiverAttempt.receiverId, "host_transport_receiver_invalid"),
159
+ sequence: positiveInteger(input.receiverAttempt.sequence, "host_transport_receiver_invalid") };
160
+ } else {
161
+ attempt = { receiverId, sequence: positiveInteger(++receiverSequence, "host_transport_receiver_invalid") };
162
+ }
163
+ const rotationLeadMs = Math.min(
164
+ Math.max(Number(input.rotationLeadMs) || DEFAULT_SESSION_ROTATION_LEAD_MS, 25),
165
+ 120_000,
166
+ );
167
+ if (rotateBeforeNextStreamSessionId) {
168
+ invalidateSession(rotateBeforeNextStreamSessionId);
169
+ rotateBeforeNextStreamSessionId = null;
170
+ }
171
+ const session = await activeSession(input.signal);
172
+ assertUsable(input.signal);
173
+ const requestSignal = input.signal ? AbortSignal.any([input.signal, lifetime.signal]) : lifetime.signal;
174
+ const controller = new AbortController();
175
+ let response;
176
+ let rotationTimer;
177
+ const cleanup = () => {
178
+ clearTimeout(rotationTimer);
179
+ requestSignal.removeEventListener("abort", abortFromCaller);
180
+ if (activeStream === slot) activeStream = null;
181
+ };
182
+ const abortFromCaller = () => controller.abort(requestSignal.reason);
183
+ controller.signal.addEventListener("abort", () => {
184
+ cleanup();
185
+ // A reader may already own the body; readSseBytes cancels that reader.
186
+ void response?.body?.cancel().catch(() => {});
187
+ }, { once: true });
188
+ if (requestSignal.aborted) abortFromCaller();
189
+ else requestSignal.addEventListener("abort", abortFromCaller, { once: true });
190
+ let handshakeTimer;
191
+ let rejectAbortedHandshake;
192
+ const handshakeDeadline = new Promise((_, reject) => {
193
+ rejectAbortedHandshake = () => reject(controller.signal.reason);
194
+ if (controller.signal.aborted) rejectAbortedHandshake();
195
+ else controller.signal.addEventListener("abort", rejectAbortedHandshake, { once: true });
196
+ handshakeTimer = setTimeout(() => controller.abort(failure("host_transport_request_timeout")), jsonRequestTimeoutMs);
197
+ });
198
+ const handshake = (async () => {
199
+ if (controller.signal.aborted) throw controller.signal.reason;
200
+ try {
201
+ response = await fetchImpl(`${baseUrl}/api/v2/host-transport/events`, {
202
+ method: "GET",
203
+ headers: {
204
+ accept: "text/event-stream",
205
+ "x-hellolynk-host-transport-version": "1",
206
+ "x-hellolynk-instance-direct-version": "1",
207
+ "x-hellolynk-native-content-version": "1",
208
+ "x-hellolynk-host-receiver-id": attempt.receiverId,
209
+ "x-hellolynk-host-receiver-sequence": String(attempt.sequence),
210
+ ...hostSessionHeaders(session),
211
+ },
212
+ signal: controller.signal,
213
+ });
214
+ } catch (error) {
215
+ throw failure(runtimeSafety.helloLynkNetworkErrorCode(error, "host_transport_request_failed"), { cause: error });
216
+ }
217
+ // Also release a late response from a fetch implementation that ignores abort.
218
+ if (controller.signal.aborted) {
219
+ await response.body?.cancel().catch(() => {});
220
+ throw controller.signal.reason;
221
+ }
222
+ if (response.status !== 200
223
+ || !String(response.headers.get("content-type") || "").startsWith("text/event-stream")
224
+ || !response.body) {
225
+ const error = await responseFailure(response, controller);
226
+ if (controller.signal.aborted) throw controller.signal.reason;
227
+ invalidateRejectedSession(error, hostSessionHeaders(session));
228
+ throw error;
229
+ }
230
+ })();
231
+ try {
232
+ await Promise.race([handshake, handshakeDeadline]);
233
+ } catch (error) {
234
+ controller.abort();
235
+ cleanup();
236
+ throw error;
237
+ } finally {
238
+ clearTimeout(handshakeTimer);
239
+ controller.signal.removeEventListener("abort", rejectAbortedHandshake);
240
+ }
241
+ if (controller.signal.aborted) throw controller.signal.reason;
242
+ let rotationRequested = false;
243
+ rotationTimer = setTimeout(() => {
244
+ rotationRequested = true;
245
+ // Persisting a received contact request may still need to ACK this SSE session.
246
+ if (latestSessionId === session.sessionId) rotateBeforeNextStreamSessionId = session.sessionId;
247
+ controller.abort();
248
+ }, Math.max(1, Date.parse(session.expiresAt) - Date.now() - rotationLeadMs));
249
+ rotationTimer.unref?.();
250
+ const events = (async function* rotateSessionEvents() {
251
+ try {
252
+ const bytes = readSseBytes(response.body, controller, streamIdleTimeoutMs);
253
+ for await (const item of routeSseEvents(parseSse(bytes), registeredEndpoints, owner)) {
254
+ if (item.event === "host_transport_closed" && [
255
+ "host_transport_session_expired", "host_transport_session_revoked",
256
+ "host_transport_credential_inactive", "host_transport_credential_revoked", "host_transport_host_revoked",
257
+ ].includes(item.data?.reason)) invalidateSession(session.sessionId);
258
+ yield item;
259
+ }
260
+ if (rotationRequested) throw failure("host_transport_session_rotation");
261
+ } catch (error) {
262
+ if (rotationRequested) throw failure("host_transport_session_rotation", { cause: error });
263
+ throw error;
264
+ } finally {
265
+ cleanup();
266
+ controller.abort();
267
+ }
268
+ })();
269
+ return Object.freeze({
270
+ response,
271
+ events,
272
+ principalId: session.principalId,
273
+ hostId: session.hostId,
274
+ async close() {
275
+ cleanup();
276
+ controller.abort();
277
+ await response.body.cancel().catch(() => {});
278
+ },
279
+ });
280
+ }
281
+
282
+ async function acknowledge(input = {}) {
283
+ requireExactFields(input, ["event", "kind", "persisted"], "host_transport_ack_input_invalid");
284
+ const event = requireRecord(input.event, "host_transport_ack_event_invalid");
285
+ const persisted = requireRecord(input.persisted, "host_transport_ack_persistence_invalid");
286
+ const kind = String(input.kind || "");
287
+ if (!new Set(["message", "contact_request"]).has(kind)) {
288
+ throw failure("host_transport_ack_kind_invalid");
289
+ }
290
+ const delegateId = safeId(event.recipientDelegateId, "host_transport_ack_delegate_invalid");
291
+ if (typeof runtimeHeadersForDelegate !== "function") {
292
+ throw failure("host_transport_runtime_resolver_required");
293
+ }
294
+ const session = await activeSession();
295
+ const runtimeHeaders = await requiredRuntimeHeaders(
296
+ await runtimeHeadersForDelegate(delegateId, { kind, event }),
297
+ );
298
+ const request = kind === "message"
299
+ ? messageAcknowledgement(event, persisted)
300
+ : contactAcknowledgement(event, persisted);
301
+ return jsonRequest(request.pathname, {
302
+ method: "POST",
303
+ headers: { ...hostSessionHeaders(session), ...runtimeHeaders },
304
+ body: request.body,
305
+ });
306
+ }
307
+
308
+ async function searchDirectory(input = {}) {
309
+ const session = await activeSession();
310
+ return jsonRequest("/api/v2/host-transport/directory/search", {
311
+ method: "POST",
312
+ headers: hostSessionHeaders(session),
313
+ body: input,
314
+ });
315
+ }
316
+
317
+ async function reserveContactRequest(input = {}) {
318
+ const { runtimeHeaders: suppliedRuntimeHeaders, ...body } = input;
319
+ const session = await activeSession();
320
+ const runtimeHeaders = await requiredRuntimeHeaders(suppliedRuntimeHeaders);
321
+ return jsonRequest("/api/v2/host-transport/contact-request-attempts", {
322
+ method: "POST",
323
+ headers: { ...hostSessionHeaders(session), ...runtimeHeaders },
324
+ body,
325
+ });
326
+ }
327
+
328
+ async function relayContactRequest(input = {}) {
329
+ const attemptId = safeId(input.attemptId, "host_transport_contact_attempt_invalid");
330
+ const { runtimeHeaders: suppliedRuntimeHeaders, attemptId: _attemptId, ...body } = input;
331
+ const session = await activeSession();
332
+ const runtimeHeaders = await requiredRuntimeHeaders(suppliedRuntimeHeaders);
333
+ return jsonRequest(
334
+ `/api/v2/host-transport/contact-request-attempts/${encodeURIComponent(attemptId)}/relay`,
335
+ {
336
+ method: "POST",
337
+ headers: { ...hostSessionHeaders(session), ...runtimeHeaders },
338
+ body,
339
+ },
340
+ );
341
+ }
342
+
343
+ async function listContactRequests(input = {}) {
344
+ if (!input || typeof input !== "object" || Array.isArray(input)
345
+ || Object.keys(input).some((key) => key !== "cursor")) {
346
+ throw failure("host_transport_contact_list_invalid");
347
+ }
348
+ const session = await activeSession();
349
+ const cursor = optionalCursor(input.cursor, "host_transport_contact_cursor_invalid");
350
+ const target = cursor == null
351
+ ? "/api/v2/host-transport/contact-requests"
352
+ : `/api/v2/host-transport/contact-requests?cursor=${encodeURIComponent(cursor)}`;
353
+ return jsonRequest(target, {
354
+ method: "GET",
355
+ headers: hostSessionHeaders(session),
356
+ });
357
+ }
358
+
359
+ async function decideContactRequest(input = {}) {
360
+ const requestId = safeId(input.requestId, "host_transport_contact_request_invalid");
361
+ const action = String(input.action || "");
362
+ if (!new Set(["accept", "reject", "block"]).has(action)) {
363
+ throw failure("host_transport_contact_decision_invalid");
364
+ }
365
+ const session = await activeSession();
366
+ return jsonRequest(
367
+ `/api/v2/host-transport/contact-requests/${encodeURIComponent(requestId)}/${action}`,
368
+ { method: "POST", headers: hostSessionHeaders(session), body: {} },
369
+ );
370
+ }
371
+
372
+ async function getContactRequestDecisionState(input = {}) {
373
+ if (!input || typeof input !== "object" || Array.isArray(input)
374
+ || Object.keys(input).length !== 1 || !Object.hasOwn(input, "requestId")) {
375
+ throw failure("host_transport_contact_request_invalid");
376
+ }
377
+ const requestId = safeId(input.requestId, "host_transport_contact_request_invalid");
378
+ const session = await activeSession();
379
+ return jsonRequest(
380
+ `/api/v2/host-transport/contact-requests/${encodeURIComponent(requestId)}/decision-state`,
381
+ { method: "GET", headers: hostSessionHeaders(session), body: undefined },
382
+ );
383
+ }
384
+
385
+ async function cancelContactRequest(input = {}) {
386
+ requireExactFields(input, ["requestId"], "host_transport_contact_request_invalid");
387
+ const requestId = safeId(input.requestId, "host_transport_contact_request_invalid");
388
+ const session = await activeSession();
389
+ return jsonRequest(
390
+ `/api/v2/host-transport/contact-requests/${encodeURIComponent(requestId)}/cancel`,
391
+ { method: "POST", headers: hostSessionHeaders(session), body: {} },
392
+ );
393
+ }
394
+
395
+ async function listRelationships() {
396
+ const session = await activeSession();
397
+ return jsonRequest("/api/v2/host-transport/relationships", {
398
+ method: "GET",
399
+ headers: hostSessionHeaders(session),
400
+ });
401
+ }
402
+
403
+ async function endRelationship(input = {}) {
404
+ requireExactFields(input, ["relationshipId"], "host_transport_relationship_invalid");
405
+ const relationshipId = safeId(input.relationshipId, "host_transport_relationship_invalid");
406
+ const session = await activeSession();
407
+ return jsonRequest(
408
+ `/api/v2/host-transport/relationships/${encodeURIComponent(relationshipId)}/end`,
409
+ { method: "POST", headers: hostSessionHeaders(session), body: {} },
410
+ );
411
+ }
412
+
413
+ async function listBlocks() {
414
+ const session = await activeSession();
415
+ return jsonRequest("/api/v2/host-transport/blocks", {
416
+ method: "GET",
417
+ headers: hostSessionHeaders(session),
418
+ });
419
+ }
420
+
421
+ async function removeBlock(input = {}) {
422
+ requireExactFields(input, ["blockId"], "host_transport_block_invalid");
423
+ const blockId = safeId(input.blockId, "host_transport_block_invalid");
424
+ const session = await activeSession();
425
+ return jsonRequest(
426
+ `/api/v2/host-transport/blocks/${encodeURIComponent(blockId)}/remove`,
427
+ { method: "POST", headers: hostSessionHeaders(session), body: {} },
428
+ );
429
+ }
430
+
431
+ async function activeSession(signal) {
432
+ assertUsable(signal);
433
+ if (issuedSessionPending) return waitForSession(issuedSessionPending, signal);
434
+ if (cachedSession && Date.parse(cachedSession.expiresAt) > Date.now() + DEFAULT_SESSION_ROTATION_LEAD_MS) {
435
+ return cachedSession;
436
+ }
437
+ if (activeSessionPending) return waitForSession(activeSessionPending, signal);
438
+ const generation = sessionGeneration;
439
+ const pending = (async () => {
440
+ let session;
441
+ try {
442
+ session = validateSession(await sessionProvider.get(), owner);
443
+ } catch (error) {
444
+ assertUsable();
445
+ if (generation !== sessionGeneration) return currentSessionOrIssue();
446
+ if (error?.code !== "host_transport_session_invalid"
447
+ && error?.code !== "host_transport_session_expired"
448
+ && error?.code !== "host_transport_session_owner_mismatch") throw error;
449
+ return issueSession();
450
+ }
451
+ assertUsable();
452
+ // An explicit rotation wins over a provider read that started earlier.
453
+ if (generation !== sessionGeneration) return currentSessionOrIssue();
454
+ if (session.sessionId === staleSessionId
455
+ || Date.parse(session.expiresAt) <= Date.now()) return issueSession();
456
+ cachedSession = session;
457
+ latestSessionId = session.sessionId;
458
+ return session;
459
+ })();
460
+ activeSessionPending = pending;
461
+ pending.finally(() => {
462
+ if (activeSessionPending === pending) activeSessionPending = null;
463
+ }).catch(() => {});
464
+ return waitForSession(pending, signal);
465
+ }
466
+
467
+ function currentSessionOrIssue() {
468
+ return issuedSessionPending || cachedSession || issueSession();
469
+ }
470
+
471
+ async function waitForSession(pending, signal) {
472
+ // Session lookup/issuance belongs to the transport, not its first caller.
473
+ // Canceling one waiter cannot cancel another admitted receive unit's ACK.
474
+ const waitSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
475
+ waitSignal.throwIfAborted();
476
+ let rejectAborted;
477
+ try {
478
+ return await Promise.race([pending, new Promise((_, reject) => {
479
+ rejectAborted = () => reject(waitSignal.reason);
480
+ waitSignal.addEventListener("abort", rejectAborted, { once: true });
481
+ })]);
482
+ } finally {
483
+ waitSignal.removeEventListener("abort", rejectAborted);
484
+ }
485
+ }
486
+
487
+ function invalidateRejectedSession(error, headers) {
488
+ if (error.httpStatus !== 401 && error.httpStatus !== 403) return;
489
+ const sessionId = headers?.["x-hellolynk-host-transport-session-id"];
490
+ if (!sessionId || latestSessionId !== sessionId) return;
491
+ cachedSession = null;
492
+ // A relationship/runtime denial does not revoke the transport session used by the live SSE receiver.
493
+ if (/^host_transport_(session|credential)_/.test(error.code)
494
+ || error.code === "host_transport_http_error") staleSessionId = sessionId;
495
+ }
496
+
497
+ function invalidateSession(sessionId) {
498
+ // A delayed rejection of an old request must not discard a newer session.
499
+ if (!sessionId || latestSessionId !== sessionId) return;
500
+ cachedSession = null;
501
+ staleSessionId = sessionId;
502
+ }
503
+
504
+ async function listAgentInstances(input = {}) {
505
+ requireExactFields(input, ["hellolynkId"], "host_transport_instance_directory_invalid");
506
+ const hellolynkId = input.hellolynkId;
507
+ if (typeof hellolynkId !== "string" || !/^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/.test(hellolynkId)) {
508
+ throw failure("host_transport_instance_directory_invalid");
509
+ }
510
+ const session = await activeSession();
511
+ const result = await jsonRequest("/api/v2/host-transport/instances/list", {
512
+ method: "POST", headers: { ...hostSessionHeaders(session),
513
+ "X-HelloLynk-Client-Types": "codex,openclaw,trae-work,workbuddy,cursor,dsh,claude-code",
514
+ }, body: { hellolynkId },
515
+ });
516
+ if (result?.ok !== true || result.hellolynkId !== hellolynkId
517
+ || !Array.isArray(result.instances) || result.instances.length > 512) {
518
+ throw failure("host_transport_instance_directory_response_invalid");
519
+ }
520
+ if (typeof result.principalId !== "string") throw failure("host_transport_instance_directory_response_invalid");
521
+ const principalId = safeId(result.principalId, "host_transport_instance_directory_response_invalid");
522
+ const ids = new Set();
523
+ const instances = result.instances.map((item) => {
524
+ if (!item || typeof item.delegateId !== "string") throw failure("host_transport_instance_directory_response_invalid");
525
+ const delegateId = safeId(item?.delegateId, "host_transport_instance_directory_response_invalid");
526
+ if (ids.has(delegateId) || item.status !== "active"
527
+ || !["codex", "openclaw", "trae-work", "workbuddy", "cursor", "dsh", "claude-code"].includes(item.clientType)
528
+ || typeof item.displayName !== "string" || !item.displayName.trim() || item.displayName.length > 80) {
529
+ throw failure("host_transport_instance_directory_response_invalid");
530
+ }
531
+ ids.add(delegateId);
532
+ // createdAt is display-only. An older relay omits it, and a malformed
533
+ // value is dropped instead of hiding the whole directory.
534
+ const createdAtMs = typeof item.createdAt === "string" && item.createdAt.length <= 64 ? Date.parse(item.createdAt) : NaN;
535
+ return { delegateId, displayName: item.displayName, clientType: item.clientType, status: "active",
536
+ ...(Number.isFinite(createdAtMs) ? { createdAt: new Date(createdAtMs).toISOString() } : {}) };
537
+ });
538
+ return { principalId, hellolynkId, instances };
539
+ }
540
+
541
+ async function jsonRequest(pathname, input) {
542
+ assertUsable(input.signal);
543
+ const requestSignal = input.signal ? AbortSignal.any([input.signal, lifetime.signal]) : lifetime.signal;
544
+ const controller = new AbortController();
545
+ const abortFromCaller = () => controller.abort(requestSignal.reason);
546
+ requestSignal.addEventListener("abort", abortFromCaller, { once: true });
547
+ if (requestSignal.aborted) abortFromCaller();
548
+ let timedOut = false;
549
+ let timeoutHandle;
550
+ const deadline = new Promise((_, reject) => {
551
+ timeoutHandle = setTimeout(() => {
552
+ timedOut = true;
553
+ controller.abort();
554
+ reject(failure("host_transport_request_timeout"));
555
+ }, jsonRequestTimeoutMs);
556
+ });
557
+ const request = (async () => {
558
+ let response;
559
+ try {
560
+ controller.signal.throwIfAborted();
561
+ response = await fetchImpl(`${baseUrl}${pathname}`, {
562
+ method: input.method,
563
+ headers: {
564
+ accept: "application/json",
565
+ "content-type": "application/json",
566
+ ...input.headers,
567
+ },
568
+ body: JSON.stringify(input.body),
569
+ signal: controller.signal,
570
+ });
571
+ } catch (error) {
572
+ if (timedOut) throw failure("host_transport_request_timeout", { cause: error });
573
+ throw failure(runtimeSafety.helloLynkNetworkErrorCode(error, "host_transport_request_failed"), { cause: error });
574
+ }
575
+ assertUsable(input.signal);
576
+ if (!response.ok) {
577
+ const error = await responseFailure(response);
578
+ invalidateRejectedSession(error, input.headers);
579
+ throw error;
580
+ }
581
+ let result;
582
+ try {
583
+ result = await response.json();
584
+ } catch (error) {
585
+ if (timedOut) throw failure("host_transport_request_timeout", { cause: error });
586
+ throw failure("host_transport_response_invalid", { cause: error });
587
+ }
588
+ assertUsable(input.signal);
589
+ return result;
590
+ })();
591
+ try {
592
+ return await Promise.race([request, deadline]);
593
+ } finally {
594
+ clearTimeout(timeoutHandle);
595
+ requestSignal.removeEventListener("abort", abortFromCaller);
596
+ }
597
+ }
598
+
599
+ function assertUsable(signal) {
600
+ lifetime.signal.throwIfAborted();
601
+ signal?.throwIfAborted();
602
+ }
603
+
604
+ return Object.freeze({
605
+ async close() {
606
+ lifetime.abort(failure("host_transport_closed"));
607
+ cachedSession = null;
608
+ registeredEndpoints.clear();
609
+ // Cloud requests may ignore abort. Only the already admitted local write
610
+ // must settle before this Host gives up its installation lock.
611
+ await sessionPersistencePending?.catch(() => {});
612
+ },
613
+ issueSession,
614
+ registerEndpoint,
615
+ openEventStream,
616
+ acknowledge,
617
+ searchDirectory,
618
+ reserveContactRequest,
619
+ relayContactRequest,
620
+ listContactRequests,
621
+ decideContactRequest,
622
+ getContactRequestDecisionState,
623
+ cancelContactRequest,
624
+ listRelationships,
625
+ listAgentInstances,
626
+ endRelationship,
627
+ listBlocks,
628
+ removeBlock,
629
+ });
630
+ }
631
+
632
+ async function* routeSseEvents(events, endpoints, owner) {
633
+ for await (const item of events) {
634
+ if (item?.event === "host_transport_ready") {
635
+ yield Object.freeze({ ...item, event: "ready" });
636
+ continue;
637
+ }
638
+ if (!item || !["message", "contact_request"].includes(item.event)) {
639
+ yield item;
640
+ continue;
641
+ }
642
+ const event = requireRecord(item.data, "host_transport_event_invalid");
643
+ const endpoint = endpoints.get(endpointRouteKey(event.recipientDeviceId, event.recipientIdentityKeyId));
644
+ if (!endpoint) throw failure("host_transport_event_endpoint_unknown");
645
+ if (item.event === "message") {
646
+ if (event.recipientDelegateId !== endpoint.delegateId || event.recipientHostId !== owner.hostId) {
647
+ throw failure("host_transport_event_endpoint_mismatch");
648
+ }
649
+ yield item;
650
+ continue;
651
+ }
652
+ if (Object.hasOwn(event, "recipientDelegateId") || Object.hasOwn(event, "recipientHostId")) {
653
+ throw failure("host_transport_contact_route_ambiguous");
654
+ }
655
+ yield Object.freeze({
656
+ event: item.event,
657
+ data: Object.freeze({
658
+ ...event,
659
+ recipientDelegateId: endpoint.delegateId,
660
+ recipientHostId: owner.hostId,
661
+ }),
662
+ });
663
+ }
664
+ }
665
+
666
+ function validateEndpointResult(value, owner) {
667
+ requireExactFields(value, ["ok", "endpoint"], "host_transport_endpoint_response_invalid");
668
+ if (value.ok !== true) throw failure("host_transport_endpoint_response_invalid");
669
+ requireExactFields(value.endpoint, [
670
+ "endpointId", "principalId", "hostId", "delegateId", "delegateAuthorizationVersion",
671
+ "deviceId", "identityKeyId",
672
+ ], "host_transport_endpoint_response_invalid");
673
+ const endpoint = Object.freeze({
674
+ endpointId: safeId(value.endpoint.endpointId, "host_transport_endpoint_response_invalid"),
675
+ principalId: safeId(value.endpoint.principalId, "host_transport_endpoint_response_invalid"),
676
+ hostId: safeId(value.endpoint.hostId, "host_transport_endpoint_response_invalid"),
677
+ delegateId: safeId(value.endpoint.delegateId, "host_transport_endpoint_response_invalid"),
678
+ delegateAuthorizationVersion: positiveInteger(
679
+ value.endpoint.delegateAuthorizationVersion,
680
+ "host_transport_endpoint_response_invalid",
681
+ ),
682
+ deviceId: safeId(value.endpoint.deviceId, "host_transport_endpoint_response_invalid"),
683
+ identityKeyId: safeId(value.endpoint.identityKeyId, "host_transport_endpoint_response_invalid"),
684
+ });
685
+ if (endpoint.principalId !== owner.principalId || endpoint.hostId !== owner.hostId) {
686
+ throw failure("host_transport_endpoint_response_mismatch");
687
+ }
688
+ return endpoint;
689
+ }
690
+
691
+ function endpointRouteKey(deviceId, identityKeyId) {
692
+ return `${safeId(deviceId, "host_transport_event_endpoint_invalid")}\0${safeId(identityKeyId, "host_transport_event_endpoint_invalid")}`;
693
+ }
694
+
695
+ function messageAcknowledgement(event, persisted) {
696
+ rejectPersistedRouteFields(persisted);
697
+ if (Object.hasOwn(persisted, "replyTargetDeleted") && (persisted.replyTargetDeleted !== true
698
+ || !event.e2eeEnvelope?.protected?.replyToClientMessageId)) throw failure("host_transport_ack_persistence_invalid");
699
+ const eventE2eeVersion = Number(event.e2eeVersion ?? event.schemaVersion);
700
+ if (eventE2eeVersion !== 2) throw failure("host_transport_ack_event_invalid");
701
+ const routeKind = oneOf(event.routeKind, ["principal_inbox", "owned_delegate", "instance_direct"], "host_transport_ack_event_invalid");
702
+ const recipientRouteVersion = event.recipientRouteVersion;
703
+ if (!Number.isSafeInteger(recipientRouteVersion)
704
+ || (routeKind === "principal_inbox" ? recipientRouteVersion < 1 : recipientRouteVersion !== 0)) {
705
+ throw failure("host_transport_ack_event_invalid");
706
+ }
707
+ return Object.freeze({
708
+ pathname: "/api/v2/relay/acks",
709
+ body: {
710
+ deliveryAttemptId: safeId(event.deliveryAttemptId, "host_transport_ack_event_invalid"),
711
+ eventId: safeId(event.eventId, "host_transport_ack_event_invalid"),
712
+ clientMessageId: safeId(event.clientMessageId, "host_transport_ack_event_invalid"),
713
+ connectionId: safeId(event.targetConnectionId, "host_transport_ack_event_invalid"),
714
+ state: persisted.replyTargetDeleted === true ? "reply_target_deleted" : "persisted_local",
715
+ envelopeDigest: safeDigest(event.envelopeDigest, "host_transport_ack_event_invalid"),
716
+ e2eeVersion: eventE2eeVersion,
717
+ routeKind,
718
+ recipientDelegateId: safeId(event.recipientDelegateId, "host_transport_ack_event_invalid"),
719
+ recipientDelegateAuthorizationVersion: positiveInteger(
720
+ event.recipientDelegateAuthorizationVersion,
721
+ "host_transport_ack_event_invalid",
722
+ ),
723
+ recipientHostId: safeId(event.recipientHostId, "host_transport_ack_event_invalid"),
724
+ recipientRouteVersion,
725
+ recipientDeviceId: safeId(event.recipientDeviceId, "host_transport_ack_event_invalid"),
726
+ recipientIdentityKeyId: safeId(event.recipientIdentityKeyId, "host_transport_ack_event_invalid"),
727
+ recipientPreKeyId: safeId(event.recipientPreKeyId, "host_transport_ack_event_invalid"),
728
+ },
729
+ });
730
+ }
731
+
732
+ function contactAcknowledgement(event, persisted) {
733
+ rejectPersistedRouteFields(persisted);
734
+ return Object.freeze({
735
+ pathname: "/api/v2/contact-request-acks",
736
+ body: {
737
+ attemptId: safeId(event.attemptId, "host_transport_ack_event_invalid"),
738
+ eventId: safeId(event.eventId, "host_transport_ack_event_invalid"),
739
+ clientRequestId: safeId(event.clientRequestId, "host_transport_ack_event_invalid"),
740
+ connectionId: safeId(event.targetConnectionId, "host_transport_ack_event_invalid"),
741
+ recipientEndpointId: safeId(event.recipientDeviceId, "host_transport_ack_event_invalid"),
742
+ envelopeDigest: safeDigest(event.envelopeDigest, "host_transport_ack_event_invalid"),
743
+ claimId: safeId(event.claimId, "host_transport_ack_event_invalid"),
744
+ state: "persisted_local",
745
+ localRecordDigest: safeDigest(persisted.localRecordDigest, "host_transport_ack_persistence_invalid"),
746
+ ackProofDigest: safeDigest(persisted.ackProofDigest, "host_transport_ack_persistence_invalid"),
747
+ },
748
+ });
749
+ }
750
+
751
+ function rejectPersistedRouteFields(value) {
752
+ const forbidden = [
753
+ "deliveryAttemptId", "attemptId", "eventId", "clientMessageId", "clientRequestId",
754
+ "connectionId", "targetConnectionId", "envelopeDigest", "e2eeVersion", "routeKind",
755
+ "recipientDelegateId", "recipientDelegateAuthorizationVersion", "recipientHostId",
756
+ "recipientRouteVersion", "recipientDeviceId", "recipientEndpointId", "recipientIdentityKeyId",
757
+ "recipientPreKeyId", "claimId", "state",
758
+ ];
759
+ if (forbidden.some((field) => Object.hasOwn(value, field))) {
760
+ throw failure("host_transport_ack_persistence_route_forbidden");
761
+ }
762
+ }
763
+
764
+ async function requiredRuntimeHeaders(provider) {
765
+ const headers = typeof provider === "function" ? await provider() : provider;
766
+ const sessionId = String(headers?.["x-hellolynk-runtime-session-id"] || "");
767
+ const sessionSecret = String(headers?.["x-hellolynk-runtime-session-secret"] || "");
768
+ if (!sessionId || !sessionSecret) throw failure("host_transport_runtime_session_required");
769
+ return {
770
+ "x-hellolynk-runtime-session-id": sessionId,
771
+ "x-hellolynk-runtime-session-secret": sessionSecret,
772
+ };
773
+ }
774
+
775
+ function hostSessionHeaders(session) {
776
+ return {
777
+ "x-hellolynk-host-transport-session-id": session.sessionId,
778
+ "x-hellolynk-host-transport-session-secret": session.sessionSecret,
779
+ };
780
+ }
781
+
782
+ async function responseFailure(response, streamController = null) {
783
+ let code = "host_transport_http_error";
784
+ try {
785
+ let body;
786
+ if (streamController && response.body) {
787
+ const decoder = new TextDecoder();
788
+ let text = "";
789
+ let size = 0;
790
+ // Keep an error response under the same cancellable handshake as its headers.
791
+ for await (const chunk of readSseBytes(response.body, streamController, DEFAULT_STREAM_IDLE_TIMEOUT_MS)) {
792
+ size += chunk.byteLength;
793
+ if (size > MAX_SSE_BUFFER_BYTES) throw failure("host_transport_response_invalid");
794
+ text += decoder.decode(chunk, { stream: true });
795
+ }
796
+ body = JSON.parse(text + decoder.decode());
797
+ } else body = await response.json();
798
+ if (/^[a-z][a-z0-9_]{0,127}$/.test(String(body?.error || ""))) code = body.error;
799
+ } catch {}
800
+ return failure(code, { httpStatus: response.status });
801
+ }
802
+
803
+ async function* readSseBytes(body, controller, idleTimeoutMs) {
804
+ const reader = body.getReader();
805
+ const signal = controller.signal;
806
+ const cancelRead = () => { void reader.cancel().catch(() => {}); };
807
+ signal.addEventListener("abort", cancelRead, { once: true });
808
+ let idleTimer;
809
+ try {
810
+ while (true) {
811
+ if (signal.aborted) throw signal.reason;
812
+ // Count comments and partial frames as activity. Time only a pending
813
+ // network read, not the caller's local persist/ACK processing of an event.
814
+ idleTimer = setTimeout(() => {
815
+ controller.abort(failure("host_transport_stream_idle_timeout"));
816
+ }, idleTimeoutMs);
817
+ let next;
818
+ try {
819
+ do {
820
+ next = await reader.read();
821
+ if (signal.aborted) throw signal.reason;
822
+ } while (!next.done && !next.value?.byteLength);
823
+ } finally {
824
+ clearTimeout(idleTimer);
825
+ }
826
+ if (next.done) return;
827
+ yield next.value;
828
+ }
829
+ } finally {
830
+ clearTimeout(idleTimer);
831
+ signal.removeEventListener("abort", cancelRead);
832
+ cancelRead();
833
+ reader.releaseLock();
834
+ }
835
+ }
836
+
837
+ async function* parseSse(body) {
838
+ const decoder = new TextDecoder();
839
+ let buffered = "";
840
+ for await (const chunk of body) {
841
+ buffered += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, "\n");
842
+ if (new TextEncoder().encode(buffered).byteLength > MAX_SSE_BUFFER_BYTES) {
843
+ throw failure("host_transport_sse_too_large");
844
+ }
845
+ while (buffered.includes("\n\n")) {
846
+ const index = buffered.indexOf("\n\n");
847
+ const block = buffered.slice(0, index);
848
+ buffered = buffered.slice(index + 2);
849
+ const event = parseSseBlock(block);
850
+ if (event) yield event;
851
+ }
852
+ }
853
+ buffered += decoder.decode();
854
+ const event = parseSseBlock(buffered);
855
+ if (event) yield event;
856
+ }
857
+
858
+ function parseSseBlock(block) {
859
+ let event = "message";
860
+ const data = [];
861
+ for (const line of String(block || "").split("\n")) {
862
+ if (line.startsWith("event:")) event = line.slice(6).trim();
863
+ else if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
864
+ }
865
+ if (data.length === 0) return null;
866
+ try {
867
+ return Object.freeze({ event, data: JSON.parse(data.join("\n")) });
868
+ } catch (error) {
869
+ throw failure("host_transport_sse_invalid", { cause: error });
870
+ }
871
+ }
872
+
873
+ function safeId(value, code) {
874
+ const normalized = String(value || "");
875
+ if (!SAFE_ID.test(normalized)) throw failure(code);
876
+ return normalized;
877
+ }
878
+
879
+ function safeDigest(value, code) {
880
+ const normalized = String(value || "");
881
+ if (!/^[a-f0-9]{64}$/.test(normalized)) throw failure(code);
882
+ return normalized;
883
+ }
884
+
885
+ function optionalCursor(value, code) {
886
+ if (value == null) return null;
887
+ const cursor = String(value);
888
+ if (!/^[A-Za-z0-9_-]{20,512}$/.test(cursor)) throw failure(code);
889
+ return cursor;
890
+ }
891
+
892
+ function positiveInteger(value, code) {
893
+ const normalized = Number(value);
894
+ if (!Number.isSafeInteger(normalized) || normalized < 1) throw failure(code);
895
+ return normalized;
896
+ }
897
+
898
+ function oneOf(value, allowed, code) {
899
+ if (!allowed.includes(value)) throw failure(code);
900
+ return value;
901
+ }
902
+
903
+ function requireRecord(value, code) {
904
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw failure(code);
905
+ return value;
906
+ }
907
+
908
+ function requireExactFields(value, fields, code) {
909
+ requireRecord(value, code);
910
+ const actual = Object.keys(value).sort();
911
+ const expected = [...fields].sort();
912
+ if (actual.length !== expected.length || actual.some((field, index) => field !== expected[index])) {
913
+ throw failure(code);
914
+ }
915
+ }
916
+
917
+ function validateSession(value, owner) {
918
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
919
+ throw failure("host_transport_session_invalid");
920
+ }
921
+ const actual = Object.keys(value).sort();
922
+ const expected = ["expiresAt", "hostId", "principalId", "sessionId", "sessionSecret"].sort();
923
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
924
+ throw failure("host_transport_session_invalid");
925
+ }
926
+ const session = Object.freeze({
927
+ sessionId: safeId(value.sessionId, "host_transport_session_invalid"),
928
+ sessionSecret: String(value.sessionSecret || ""),
929
+ principalId: safeId(value.principalId, "host_transport_session_invalid"),
930
+ hostId: safeId(value.hostId, "host_transport_session_invalid"),
931
+ expiresAt: String(value.expiresAt || ""),
932
+ });
933
+ if (!/^hosttransportsession_[A-Za-z0-9_-]{16,}$/.test(session.sessionId)
934
+ || !/^hostsession_[a-f0-9]{48}$/.test(session.sessionSecret)
935
+ || !Number.isFinite(Date.parse(session.expiresAt))) {
936
+ throw failure("host_transport_session_invalid");
937
+ }
938
+ if (session.principalId !== owner.principalId || session.hostId !== owner.hostId) {
939
+ throw failure("host_transport_session_owner_mismatch");
940
+ }
941
+ return session;
942
+ }
943
+
944
+ function failure(code, options = {}) {
945
+ return new HelloLynkHostTransportClientError(code, options);
946
+ }