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

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 +86 -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 +160 -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,770 @@
1
+ import runtimeSafety from "./hellolynk-runtime-safety.cjs";
2
+ const DEFAULT_TIMEOUT_MS = 10_000;
3
+ const MAX_JSON_RESPONSE_BYTES = 256 * 1024;
4
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
5
+ const DEVICE_RUNTIME_TTL_MS = 20 * 60 * 1000;
6
+ const DEVICE_RUNTIME_RECOVERY_WINDOW_MS = 120_000;
7
+ const DEVICE_RUNTIME_SCOPES = Object.freeze([
8
+ "relay.text.receive",
9
+ "relay.text.send",
10
+ "runtime.session.refresh",
11
+ "runtime.session.revoke",
12
+ ]);
13
+ export const RECEIVE_ONLY_RUNTIME_SCOPES = Object.freeze(["relay.text.receive"]);
14
+ const INVALIDATING_RUNTIME_SESSION_REASONS = new Set([
15
+ "runtime_session_expired",
16
+ "runtime_session_revoked",
17
+ "runtime_session_superseded",
18
+ "runtime_session_missing",
19
+ "runtime_session_invalid",
20
+ "runtime_session_not_found",
21
+ "invalid_runtime_session_handle",
22
+ ]);
23
+
24
+ export class HelloLynkClientError extends Error {
25
+ constructor(code, options = {}) {
26
+ super(code, options.cause ? { cause: options.cause } : undefined);
27
+ this.name = "HelloLynkClientError";
28
+ this.code = code;
29
+ this.httpStatus = options.httpStatus || null;
30
+ this.retryable = options.retryable === true;
31
+ }
32
+ }
33
+
34
+ export function normalizeHelloLynkBaseUrl(value, options = {}) {
35
+ let url;
36
+ try {
37
+ url = new URL(String(value || ""));
38
+ } catch {
39
+ throw new HelloLynkClientError("invalid_hellolynk_base_url");
40
+ }
41
+ if (url.username || url.password || url.search || url.hash) {
42
+ throw new HelloLynkClientError("unsafe_hellolynk_base_url");
43
+ }
44
+ const loopback = LOOPBACK_HOSTS.has(url.hostname);
45
+ if (url.protocol !== "https:" && !(options.allowLoopbackHttp === true && loopback && url.protocol === "http:")) {
46
+ throw new HelloLynkClientError("https_required");
47
+ }
48
+ url.pathname = url.pathname.replace(/\/+$/, "");
49
+ return url.toString().replace(/\/$/, "");
50
+ }
51
+
52
+ function assertBindingKey(value) {
53
+ const key = String(value || "");
54
+ if (!/^hellolynk_bind_v1_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{32,}$/.test(key)) {
55
+ throw new HelloLynkClientError("invalid_binding_key_format");
56
+ }
57
+ return key;
58
+ }
59
+
60
+ function assertDeviceId(value) {
61
+ const deviceId = String(value || "").trim();
62
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/.test(deviceId)) {
63
+ throw new HelloLynkClientError("invalid_device_id");
64
+ }
65
+ return deviceId;
66
+ }
67
+
68
+ function assertDeviceCredential(value) {
69
+ const credential = String(value || "");
70
+ if (!/^hellolynk_device_v1_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{32,}$/.test(credential)) {
71
+ throw new HelloLynkClientError("invalid_device_credential_format");
72
+ }
73
+ return credential;
74
+ }
75
+
76
+ function assertSafeId(value, code) {
77
+ const normalized = String(value || "").trim();
78
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/.test(normalized)) {
79
+ throw new HelloLynkClientError(code);
80
+ }
81
+ return normalized;
82
+ }
83
+
84
+ async function safeJson(response) {
85
+ try {
86
+ const declaredLength = Number(response.headers.get("content-length"));
87
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_JSON_RESPONSE_BYTES) {
88
+ // We own this unread body. Initiate disposal without waiting for a
89
+ // transport's cancellation to finish or replacing the diagnostic.
90
+ try { void Promise.resolve(response.body?.cancel()).catch(() => {}); } catch {}
91
+ return null;
92
+ }
93
+ if (!response.body || typeof response.body.getReader !== "function") return null;
94
+ const reader = response.body.getReader();
95
+ const chunks = [];
96
+ let total = 0;
97
+ let complete = false;
98
+ try {
99
+ while (true) {
100
+ const { done, value } = await reader.read();
101
+ if (done) { complete = true; break; }
102
+ total += value.byteLength;
103
+ if (total > MAX_JSON_RESPONSE_BYTES) return null;
104
+ chunks.push(value);
105
+ }
106
+ } finally {
107
+ if (!complete) {
108
+ try { void Promise.resolve(reader.cancel()).catch(() => {}); } catch {}
109
+ }
110
+ reader.releaseLock();
111
+ }
112
+ const body = new Uint8Array(total);
113
+ let offset = 0;
114
+ for (const chunk of chunks) {
115
+ body.set(chunk, offset);
116
+ offset += chunk.byteLength;
117
+ }
118
+ return JSON.parse(new TextDecoder().decode(body));
119
+ } catch (error) {
120
+ if (error?.name === "AbortError") throw error;
121
+ return null;
122
+ }
123
+ }
124
+
125
+ function safeRemoteErrorCode(value) {
126
+ const code = String(value || "");
127
+ return /^[a-z][a-z0-9_]{0,63}$/.test(code) ? code : "binding_key_redeem_failed";
128
+ }
129
+
130
+ function safeDeviceRuntimeErrorCode(value) {
131
+ const code = String(value || "");
132
+ return /^[a-z][a-z0-9_]{0,63}$/.test(code) ? code : "device_runtime_session_issue_failed";
133
+ }
134
+
135
+ function normalizeRequestedRuntimeScopes(value) {
136
+ if (value == null) return DEVICE_RUNTIME_SCOPES;
137
+ if (
138
+ !Array.isArray(value) ||
139
+ value.length !== RECEIVE_ONLY_RUNTIME_SCOPES.length ||
140
+ !RECEIVE_ONLY_RUNTIME_SCOPES.every((scope, index) => value[index] === scope)
141
+ ) throw new HelloLynkClientError("invalid_requested_runtime_scopes");
142
+ return RECEIVE_ONLY_RUNTIME_SCOPES;
143
+ }
144
+
145
+ function exactRuntimeScopes(actual, expected) {
146
+ return actual.length === expected.length && expected.every((scope, index) => actual[index] === scope);
147
+ }
148
+
149
+ function assertRuntimeSessionHandle(
150
+ value,
151
+ expectedAgentId,
152
+ expectedPrincipalId,
153
+ nowMs = Date.now(),
154
+ expectedScopes = DEVICE_RUNTIME_SCOPES,
155
+ ) {
156
+ const sessionId = assertSafeId(value?.sessionId, "invalid_runtime_session_handle");
157
+ const runtimeId = assertSafeId(value?.runtimeId, "invalid_runtime_session_handle");
158
+ const principalId = assertSafeId(value?.principalId, "invalid_runtime_session_handle");
159
+ const agentId = assertSafeId(value?.agentId, "invalid_runtime_session_handle");
160
+ const delegateId = value?.delegateId == null ? null
161
+ : assertSafeId(value.delegateId, "invalid_runtime_session_handle");
162
+ const hostId = value?.hostId == null ? null
163
+ : assertSafeId(value.hostId, "invalid_runtime_session_handle");
164
+ const authorizationVersion = value?.authorizationVersion == null ? null : Number(value.authorizationVersion);
165
+ const routeFieldCount = [delegateId, hostId, authorizationVersion].filter((item) => item != null).length;
166
+ const deviceId = assertSafeId(value?.deviceId, "invalid_runtime_session_handle");
167
+ const sessionSecret = String(value?.sessionSecret || "");
168
+ const expiresAtMs = Number(value?.expiresAtMs);
169
+ const allowedScopes = Array.isArray(value?.allowedScopes) ? value.allowedScopes.map(String) : [];
170
+ const exactScopes = exactRuntimeScopes(allowedScopes, expectedScopes);
171
+ if (
172
+ !/^runtime_[a-f0-9]{48}$/.test(sessionSecret) ||
173
+ principalId !== expectedPrincipalId ||
174
+ agentId !== expectedAgentId ||
175
+ !exactScopes ||
176
+ (routeFieldCount !== 0 && routeFieldCount !== 3) ||
177
+ (routeFieldCount === 3 && (!Number.isSafeInteger(authorizationVersion) || authorizationVersion < 1)) ||
178
+ !Number.isFinite(expiresAtMs) ||
179
+ expiresAtMs <= nowMs
180
+ ) throw new HelloLynkClientError("invalid_runtime_session_handle");
181
+ return {
182
+ sessionId,
183
+ sessionSecret,
184
+ runtimeId,
185
+ principalId,
186
+ agentId,
187
+ delegateId,
188
+ hostId,
189
+ authorizationVersion,
190
+ deviceId,
191
+ allowedScopes: [...allowedScopes],
192
+ expiresAtMs,
193
+ };
194
+ }
195
+
196
+ // A recently expired predecessor is only a recovery proof for the issue endpoint.
197
+ // Keep ordinary session validation strict so this cannot authorize business calls.
198
+ function assertRuntimeRecoveryProof(value, expectedAgentId, expectedPrincipalId, nowMs, expectedScopes) {
199
+ return assertRuntimeSessionHandle(
200
+ value, expectedAgentId, expectedPrincipalId,
201
+ nowMs - DEVICE_RUNTIME_RECOVERY_WINDOW_MS, expectedScopes,
202
+ );
203
+ }
204
+
205
+ export function shouldInvalidateDeviceRuntimeSession(error) {
206
+ const candidates = [error?.reason, error?.code]
207
+ .map((value) => String(value || "").trim())
208
+ .filter(Boolean);
209
+ return candidates.some((value) => {
210
+ if (INVALIDATING_RUNTIME_SESSION_REASONS.has(value)) return true;
211
+ const wrapped = value.match(
212
+ /^relay_event_stream_closed_(runtime_session_(?:expired|revoked|superseded|missing|invalid|not_found))$/,
213
+ );
214
+ return Boolean(wrapped && INVALIDATING_RUNTIME_SESSION_REASONS.has(wrapped[1]));
215
+ });
216
+ }
217
+
218
+ async function boundedJsonFetch(fetchImpl, url, options, timeoutMs) {
219
+ const controller = new AbortController();
220
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
221
+ try {
222
+ const response = await fetchImpl(url, { ...options, signal: controller.signal });
223
+ const result = await safeJson(response);
224
+ return { response, result };
225
+ } catch (error) {
226
+ if (error?.name === "AbortError") throw new HelloLynkClientError("request_timeout", { retryable: true });
227
+ throw new HelloLynkClientError(runtimeSafety.helloLynkNetworkErrorCode(error), { cause: error, retryable: true });
228
+ } finally {
229
+ clearTimeout(timer);
230
+ }
231
+ }
232
+
233
+ export async function redeemAgentBindingKey(options) {
234
+ const baseUrl = normalizeHelloLynkBaseUrl(options?.baseUrl, {
235
+ allowLoopbackHttp: options?.allowLoopbackHttp === true,
236
+ });
237
+ const bindingKey = assertBindingKey(options?.bindingKey);
238
+ const deviceId = assertDeviceId(options?.deviceId);
239
+ const hostInstallationId = assertSafeId(
240
+ options?.hostInstallationId,
241
+ "invalid_host_installation_id",
242
+ );
243
+ const fetchImpl = options?.fetchImpl || runtimeSafety.helloLynkNetworkFetch;
244
+ if (typeof fetchImpl !== "function") throw new HelloLynkClientError("fetch_unavailable");
245
+ const timeoutMs = Math.min(Math.max(Number(options?.timeoutMs) || DEFAULT_TIMEOUT_MS, 1_000), 30_000);
246
+ const endpoint = `${baseUrl}/api/v1/public-preview/agent-binding-keys/redeem`;
247
+ const { response, result } = await boundedJsonFetch(fetchImpl, endpoint, {
248
+ method: "POST",
249
+ headers: { "content-type": "application/json" },
250
+ body: JSON.stringify({
251
+ bindingKey,
252
+ deviceId,
253
+ hostInstallationId,
254
+ ...(options?.bootstrapHostTransport === true ? { bootstrapHostTransport: true } : {}),
255
+ }),
256
+ redirect: "error",
257
+ }, timeoutMs);
258
+ if (!response.ok) {
259
+ throw new HelloLynkClientError(safeRemoteErrorCode(result?.error), { httpStatus: response.status });
260
+ }
261
+ if (
262
+ typeof result?.deviceCredential !== "string" ||
263
+ typeof result?.deviceCredentialId !== "string" ||
264
+ typeof result?.hellolynkId !== "string" ||
265
+ typeof result?.delegateId !== "string" ||
266
+ result.deviceId !== deviceId ||
267
+ result.credentialReturnedOnce !== true
268
+ ) {
269
+ throw new HelloLynkClientError("invalid_redeem_response");
270
+ }
271
+ const redeemRouteFieldCount = [result.hostId, result.authorizationVersion]
272
+ .filter((value) => value != null).length;
273
+ if (redeemRouteFieldCount !== 0 && redeemRouteFieldCount !== 2) {
274
+ throw new HelloLynkClientError("invalid_redeem_response");
275
+ }
276
+ const hostId = redeemRouteFieldCount === 2
277
+ ? assertSafeId(result.hostId, "invalid_redeem_response") : null;
278
+ const authorizationVersion = redeemRouteFieldCount === 2
279
+ ? Number(result.authorizationVersion) : null;
280
+ if (redeemRouteFieldCount === 2 && (
281
+ !Number.isSafeInteger(authorizationVersion) || authorizationVersion < 1
282
+ )) throw new HelloLynkClientError("invalid_redeem_response");
283
+ const bootstrapHostTransportCredential = result.bootstrapHostTransportCredential == null
284
+ ? null : normalizeBootstrapHostTransportCredential(
285
+ result.bootstrapHostTransportCredential,
286
+ hostId,
287
+ result.bootstrapHostTransportCredentialReturnedOnce,
288
+ );
289
+ return {
290
+ credential: result.deviceCredential,
291
+ credentialId: result.deviceCredentialId,
292
+ principalId: result.principalId,
293
+ hellolynkId: assertSafeId(result.hellolynkId, "invalid_redeem_response"),
294
+ agentId: result.agentId,
295
+ delegateId: assertSafeId(result.delegateId, "invalid_redeem_response"),
296
+ hostId,
297
+ authorizationVersion,
298
+ deviceId: result.deviceId,
299
+ region: result.region,
300
+ allowedScopes: Array.isArray(result.allowedScopes) ? [...result.allowedScopes] : [],
301
+ bootstrapHostTransportCredential,
302
+ };
303
+ }
304
+
305
+ function normalizeBootstrapHostTransportCredential(value, hostId, returnedOnce) {
306
+ if (!hostId || returnedOnce !== true || !value || typeof value !== "object" || Array.isArray(value)
307
+ || Object.keys(value).sort().join(",") !== "createdAt,credential,credentialId,expiresAt,hostId") {
308
+ throw new HelloLynkClientError("invalid_redeem_response");
309
+ }
310
+ const credentialId = assertSafeId(value.credentialId, "invalid_redeem_response");
311
+ const credential = String(value.credential || "");
312
+ const createdAt = String(value.createdAt || "");
313
+ const expiresAt = String(value.expiresAt || "");
314
+ if (value.hostId !== hostId
315
+ || !new RegExp(`^hellolynk_host_transport_v1_${credentialId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[A-Za-z0-9_-]{32,}$`).test(credential)
316
+ || !Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(expiresAt))
317
+ || Date.parse(expiresAt) <= Date.parse(createdAt)) {
318
+ throw new HelloLynkClientError("invalid_redeem_response");
319
+ }
320
+ return Object.freeze({ credentialId, hostId, credential, createdAt, expiresAt });
321
+ }
322
+
323
+ export async function issueDeviceRuntimeSession(options) {
324
+ const baseUrl = normalizeHelloLynkBaseUrl(options?.baseUrl, {
325
+ allowLoopbackHttp: options?.allowLoopbackHttp === true,
326
+ });
327
+ const deviceCredential = assertDeviceCredential(options?.deviceCredential);
328
+ const expectedPrincipalId = assertSafeId(options?.expectedPrincipalId, "invalid_expected_principal_id");
329
+ const expectedAgentId = assertSafeId(options?.expectedAgentId, "invalid_expected_agent_id");
330
+ const expectsRoute = options?.expectedDelegateId != null || options?.expectedHostId != null
331
+ || options?.expectedAuthorizationVersion != null;
332
+ if (expectsRoute && (
333
+ options?.expectedDelegateId == null || options?.expectedHostId == null
334
+ || options?.expectedAuthorizationVersion == null
335
+ )) throw new HelloLynkClientError("invalid_expected_runtime_route");
336
+ const expectedDelegateId = expectsRoute
337
+ ? assertSafeId(options.expectedDelegateId, "invalid_expected_runtime_route") : null;
338
+ const expectedHostId = expectsRoute
339
+ ? assertSafeId(options.expectedHostId, "invalid_expected_runtime_route") : null;
340
+ const expectedAuthorizationVersion = expectsRoute ? Number(options.expectedAuthorizationVersion) : null;
341
+ if (expectsRoute && (!Number.isSafeInteger(expectedAuthorizationVersion) || expectedAuthorizationVersion < 1)) {
342
+ throw new HelloLynkClientError("invalid_expected_runtime_route");
343
+ }
344
+ const expectedRuntimeType = assertSafeId(
345
+ options?.expectedRuntimeType || "hellolynk_client",
346
+ "invalid_expected_runtime_type",
347
+ );
348
+ const requestedScopes = normalizeRequestedRuntimeScopes(options?.requestedScopes);
349
+ const fetchImpl = options?.fetchImpl || runtimeSafety.helloLynkNetworkFetch;
350
+ if (typeof fetchImpl !== "function") throw new HelloLynkClientError("fetch_unavailable");
351
+ const timeoutMs = Math.min(Math.max(Number(options?.timeoutMs) || DEFAULT_TIMEOUT_MS, 1_000), 30_000);
352
+ const endpoint = `${baseUrl}/api/v1/public-preview/device-credentials/runtime-sessions`;
353
+ const requestedAt = Date.now();
354
+ const previous = options.previousSession == null ? null : assertRuntimeRecoveryProof(
355
+ options.previousSession, expectedAgentId, expectedPrincipalId, requestedAt, requestedScopes,
356
+ );
357
+ if (previous && expectsRoute && (previous.delegateId !== expectedDelegateId
358
+ || previous.hostId !== expectedHostId || previous.authorizationVersion !== expectedAuthorizationVersion)) {
359
+ throw new HelloLynkClientError("invalid_expected_runtime_route");
360
+ }
361
+ const explicitReceiveOnly = requestedScopes === RECEIVE_ONLY_RUNTIME_SCOPES;
362
+ const { response, result } = await boundedJsonFetch(fetchImpl, endpoint, {
363
+ method: "POST",
364
+ headers: {
365
+ accept: "application/json",
366
+ ...(explicitReceiveOnly ? { "content-type": "application/json" } : {}),
367
+ "x-hellolynk-device-credential": deviceCredential,
368
+ ...(previous ? {
369
+ "x-hellolynk-runtime-session-id": previous.sessionId,
370
+ "x-hellolynk-runtime-session-secret": previous.sessionSecret,
371
+ } : {}),
372
+ },
373
+ ...(explicitReceiveOnly ? { body: JSON.stringify({ allowedScopes: [...requestedScopes] }) } : {}),
374
+ redirect: "error",
375
+ }, timeoutMs);
376
+ if (!response.ok) {
377
+ // The authenticated server has confirmed that this instance has no active
378
+ // lease. One fresh session request still passes its clone/conflict checks.
379
+ // A lost response, an ordinary 401 or an active successor never takes this path.
380
+ if (previous && response.status === 401 && result?.error === "runtime_session_recovery_unneeded") {
381
+ const fresh = await issueDeviceRuntimeSession({ ...options, previousSession: null });
382
+ if (fresh.deviceId !== previous.deviceId || fresh.delegateId !== previous.delegateId
383
+ || fresh.hostId !== previous.hostId || fresh.authorizationVersion !== previous.authorizationVersion) {
384
+ throw new HelloLynkClientError("invalid_device_runtime_response");
385
+ }
386
+ return fresh;
387
+ }
388
+ throw new HelloLynkClientError(safeDeviceRuntimeErrorCode(result?.error), {
389
+ httpStatus: response.status,
390
+ retryable: response.status === 408 || response.status === 425 || response.status === 429 || response.status >= 500,
391
+ });
392
+ }
393
+
394
+ const session = result?.runtimeSession;
395
+ const sessionId = assertSafeId(session?.id, "invalid_device_runtime_response");
396
+ const runtimeId = assertSafeId(session?.runtimeId, "invalid_device_runtime_response");
397
+ const principalId = assertSafeId(session?.principalId, "invalid_device_runtime_response");
398
+ const agentId = assertSafeId(session?.agentId, "invalid_device_runtime_response");
399
+ const deviceId = assertSafeId(session?.deviceId, "invalid_device_runtime_response");
400
+ const routeFieldCount = [session?.delegateId, session?.hostId, session?.authorizationVersion]
401
+ .filter((value) => value != null).length;
402
+ if (routeFieldCount !== 0 && routeFieldCount !== 3) throw new HelloLynkClientError("invalid_device_runtime_response");
403
+ const delegateId = routeFieldCount === 3
404
+ ? assertSafeId(session.delegateId, "invalid_device_runtime_response") : null;
405
+ const hostId = routeFieldCount === 3
406
+ ? assertSafeId(session.hostId, "invalid_device_runtime_response") : null;
407
+ const authorizationVersion = routeFieldCount === 3 ? Number(session.authorizationVersion) : null;
408
+ const secret = String(result?.runtimeSessionSecret || "");
409
+ const expiresAtMs = Date.parse(String(session?.expiresAt || ""));
410
+ const scopes = Array.isArray(session?.allowedScopes) ? session.allowedScopes.map(String) : [];
411
+ const exactScopes = exactRuntimeScopes(scopes, requestedScopes);
412
+ const recoverableRotation = result?.secretReturnedOnce === false
413
+ && result?.recoveryWindowMs === DEVICE_RUNTIME_RECOVERY_WINDOW_MS;
414
+ const recoveredCreatedAtMs = expiresAtMs - DEVICE_RUNTIME_TTL_MS;
415
+ const validSecretDelivery = recoverableRotation
416
+ ? previous != null && routeFieldCount === 3 && previous.delegateId != null
417
+ && recoveredCreatedAtMs > requestedAt - DEVICE_RUNTIME_RECOVERY_WINDOW_MS - 5 * 60_000
418
+ && recoveredCreatedAtMs <= previous.expiresAtMs
419
+ // An active device credential can receive a fresh session even when its
420
+ // previous proof has expired (for example without an instance lease).
421
+ // This is a new authenticated issue, not recovery of an old secret. The
422
+ // exact owner, device, route, scopes and lifetime are still checked below.
423
+ : result?.secretReturnedOnce === true && result?.recoveryWindowMs == null;
424
+ if (
425
+ !/^runtime_[a-f0-9]{48}$/.test(secret) ||
426
+ !validSecretDelivery ||
427
+ result?.previewOnly !== true ||
428
+ result?.expiresInMs !== DEVICE_RUNTIME_TTL_MS ||
429
+ session?.sessionState !== "active" ||
430
+ session?.runtimeType !== expectedRuntimeType ||
431
+ session?.executionMode !== "receive_only" ||
432
+ session?.signatureRequired !== false ||
433
+ !exactScopes ||
434
+ !Number.isFinite(expiresAtMs) ||
435
+ expiresAtMs <= requestedAt ||
436
+ expiresAtMs > requestedAt + DEVICE_RUNTIME_TTL_MS + 5 * 60_000 ||
437
+ principalId !== expectedPrincipalId ||
438
+ agentId !== expectedAgentId ||
439
+ (previous && (
440
+ deviceId !== previous.deviceId || delegateId !== previous.delegateId
441
+ || hostId !== previous.hostId || authorizationVersion !== previous.authorizationVersion
442
+ )) ||
443
+ (routeFieldCount === 3 && (!Number.isSafeInteger(authorizationVersion) || authorizationVersion < 1)) ||
444
+ (expectsRoute && (
445
+ delegateId !== expectedDelegateId || hostId !== expectedHostId
446
+ || authorizationVersion !== expectedAuthorizationVersion
447
+ ))
448
+ ) {
449
+ throw new HelloLynkClientError("invalid_device_runtime_response");
450
+ }
451
+
452
+ return {
453
+ sessionId,
454
+ sessionSecret: secret,
455
+ runtimeId,
456
+ runtimeType: expectedRuntimeType,
457
+ principalId,
458
+ agentId,
459
+ delegateId,
460
+ hostId,
461
+ authorizationVersion,
462
+ deviceId,
463
+ allowedScopes: [...scopes],
464
+ expiresAt: new Date(expiresAtMs).toISOString(),
465
+ expiresAtMs,
466
+ };
467
+ }
468
+
469
+ export async function preflightDeviceRuntimeSession(options) {
470
+ const baseUrl = normalizeHelloLynkBaseUrl(options?.baseUrl, {
471
+ allowLoopbackHttp: options?.allowLoopbackHttp === true,
472
+ });
473
+ const expectedPrincipalId = assertSafeId(options?.expectedPrincipalId, "invalid_expected_principal_id");
474
+ const expectedAgentId = assertSafeId(options?.expectedAgentId, "invalid_expected_agent_id");
475
+ const expectsRoute = options?.expectedDelegateId != null || options?.expectedHostId != null
476
+ || options?.expectedAuthorizationVersion != null;
477
+ if (expectsRoute && (
478
+ options?.expectedDelegateId == null || options?.expectedHostId == null
479
+ || options?.expectedAuthorizationVersion == null
480
+ )) throw new HelloLynkClientError("invalid_expected_runtime_route");
481
+ const expectedDelegateId = expectsRoute
482
+ ? assertSafeId(options.expectedDelegateId, "invalid_expected_runtime_route") : null;
483
+ const expectedHostId = expectsRoute
484
+ ? assertSafeId(options.expectedHostId, "invalid_expected_runtime_route") : null;
485
+ const expectedAuthorizationVersion = expectsRoute ? Number(options.expectedAuthorizationVersion) : null;
486
+ const requestedScopes = normalizeRequestedRuntimeScopes(options?.requestedScopes);
487
+ const session = assertRuntimeSessionHandle(
488
+ options?.session,
489
+ expectedAgentId,
490
+ expectedPrincipalId,
491
+ Date.now(),
492
+ requestedScopes,
493
+ );
494
+ const fetchImpl = options?.fetchImpl || runtimeSafety.helloLynkNetworkFetch;
495
+ if (typeof fetchImpl !== "function") throw new HelloLynkClientError("fetch_unavailable");
496
+ const timeoutMs = Math.min(Math.max(Number(options?.timeoutMs) || DEFAULT_TIMEOUT_MS, 1_000), 30_000);
497
+ const { response, result } = await boundedJsonFetch(fetchImpl, `${baseUrl}/api/v1/runtime/session`, {
498
+ method: "GET",
499
+ headers: {
500
+ accept: "application/json",
501
+ "x-hellolynk-runtime-session-id": session.sessionId,
502
+ "x-hellolynk-runtime-session-secret": session.sessionSecret,
503
+ },
504
+ redirect: "error",
505
+ }, timeoutMs);
506
+ if (!response.ok) {
507
+ throw new HelloLynkClientError(safeDeviceRuntimeErrorCode(result?.error), {
508
+ httpStatus: response.status,
509
+ retryable: response.status === 408 || response.status === 425 || response.status === 429 || response.status >= 500,
510
+ });
511
+ }
512
+ const remoteSession = result?.runtimeSession;
513
+ const remoteScopes = Array.isArray(remoteSession?.allowedScopes)
514
+ ? remoteSession.allowedScopes.map(String) : [];
515
+ const remoteRouteFieldCount = [
516
+ remoteSession?.delegateId, remoteSession?.hostId, remoteSession?.authorizationVersion,
517
+ ].filter((value) => value != null).length;
518
+ const localRouteFieldCount = [session.delegateId, session.hostId, session.authorizationVersion]
519
+ .filter((value) => value != null).length;
520
+ const remoteAuthorizationVersion = remoteRouteFieldCount === 3
521
+ ? Number(remoteSession.authorizationVersion) : null;
522
+ const remoteRouteValid = remoteRouteFieldCount !== 3 || (
523
+ /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/.test(String(remoteSession.delegateId || "").trim())
524
+ && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/.test(String(remoteSession.hostId || "").trim())
525
+ && Number.isSafeInteger(remoteAuthorizationVersion)
526
+ && remoteAuthorizationVersion >= 1
527
+ );
528
+ if (
529
+ remoteSession?.id !== session.sessionId ||
530
+ remoteSession?.runtimeId !== session.runtimeId ||
531
+ remoteSession?.principalId !== expectedPrincipalId ||
532
+ remoteSession?.agentId !== expectedAgentId ||
533
+ remoteSession?.deviceId !== session.deviceId ||
534
+ !exactRuntimeScopes(remoteScopes, requestedScopes) ||
535
+ remoteRouteFieldCount !== localRouteFieldCount ||
536
+ !remoteRouteValid ||
537
+ (remoteRouteFieldCount === 3 && (
538
+ remoteSession.delegateId !== session.delegateId
539
+ || remoteSession.hostId !== session.hostId
540
+ || remoteAuthorizationVersion !== session.authorizationVersion
541
+ )) ||
542
+ (expectsRoute && (
543
+ remoteSession?.delegateId !== expectedDelegateId || remoteSession?.hostId !== expectedHostId
544
+ || Number(remoteSession?.authorizationVersion) !== expectedAuthorizationVersion
545
+ || session.delegateId !== expectedDelegateId || session.hostId !== expectedHostId
546
+ || session.authorizationVersion !== expectedAuthorizationVersion
547
+ )) ||
548
+ remoteSession?.sessionState !== "active" ||
549
+ (remoteRouteFieldCount !== 0 && remoteRouteFieldCount !== 3) ||
550
+ result?.runtime?.id !== session.runtimeId ||
551
+ result?.agent?.id !== expectedAgentId
552
+ ) throw new HelloLynkClientError("invalid_runtime_session_preflight_response");
553
+ return {
554
+ ok: true,
555
+ principalId: expectedPrincipalId,
556
+ agentId: expectedAgentId,
557
+ ...(expectsRoute ? {
558
+ delegateId: expectedDelegateId,
559
+ hostId: expectedHostId,
560
+ authorizationVersion: expectedAuthorizationVersion,
561
+ } : {}),
562
+ expiresAt: new Date(session.expiresAtMs).toISOString(),
563
+ };
564
+ }
565
+
566
+ export function createDeviceRuntimeSessionManager(options = {}) {
567
+ const issue = options.issue || issueDeviceRuntimeSession;
568
+ const now = options.now || (() => Date.now());
569
+ const sessionStore = options.sessionStore || null;
570
+ const sessionKey = options.sessionKey == null ? null : String(options.sessionKey);
571
+ if (sessionStore && (
572
+ !sessionKey ||
573
+ typeof sessionStore.load !== "function" ||
574
+ typeof sessionStore.save !== "function" ||
575
+ typeof sessionStore.remove !== "function" ||
576
+ typeof sessionStore.withLock !== "function"
577
+ )) throw new HelloLynkClientError("invalid_runtime_session_store");
578
+ const minimumRemainingMs = Math.min(
579
+ Math.max(Number(options.minimumRemainingMs) || 2 * 60 * 1000, 30_000),
580
+ 5 * 60 * 1000,
581
+ );
582
+ let current = null;
583
+
584
+ const routeMatches = (session, input) => {
585
+ const expectedValues = [
586
+ input?.expectedDelegateId, input?.expectedHostId, input?.expectedAuthorizationVersion,
587
+ ];
588
+ const expectedCount = expectedValues.filter((value) => value != null).length;
589
+ if (expectedCount === 0) return true;
590
+ if (expectedCount !== 3) throw new HelloLynkClientError("invalid_expected_runtime_route");
591
+ return session.delegateId === String(input.expectedDelegateId)
592
+ && session.hostId === String(input.expectedHostId)
593
+ && session.authorizationVersion === Number(input.expectedAuthorizationVersion);
594
+ };
595
+
596
+ const scopesMatch = (session, input) => {
597
+ const expectedScopes = normalizeRequestedRuntimeScopes(input?.requestedScopes);
598
+ return exactRuntimeScopes(session.allowedScopes, expectedScopes);
599
+ };
600
+
601
+ const reusable = (candidate, input, remainingMs = minimumRemainingMs) => {
602
+ try {
603
+ if (!candidate) return null;
604
+ const normalized = assertRuntimeSessionHandle(
605
+ candidate,
606
+ input?.expectedAgentId == null
607
+ ? candidate.agentId : assertSafeId(input.expectedAgentId, "invalid_expected_agent_id"),
608
+ input?.expectedPrincipalId == null
609
+ ? candidate.principalId : assertSafeId(input.expectedPrincipalId, "invalid_expected_principal_id"),
610
+ now(),
611
+ normalizeRequestedRuntimeScopes(input?.requestedScopes),
612
+ );
613
+ return normalized.expiresAtMs - now() > remainingMs
614
+ && routeMatches(normalized, input) && scopesMatch(normalized, input)
615
+ ? normalized : null;
616
+ } catch (error) {
617
+ if (error?.code === "invalid_runtime_session_handle") return null;
618
+ throw error;
619
+ }
620
+ };
621
+
622
+ const recoveryProof = (candidate, input) => {
623
+ if (!candidate) return null;
624
+ try {
625
+ const normalized = assertRuntimeRecoveryProof(
626
+ candidate,
627
+ input?.expectedAgentId == null
628
+ ? candidate.agentId : assertSafeId(input.expectedAgentId, "invalid_expected_agent_id"),
629
+ input?.expectedPrincipalId == null
630
+ ? candidate.principalId : assertSafeId(input.expectedPrincipalId, "invalid_expected_principal_id"),
631
+ now(), normalizeRequestedRuntimeScopes(input?.requestedScopes),
632
+ );
633
+ return routeMatches(normalized, input) && scopesMatch(normalized, input) ? normalized : null;
634
+ } catch (error) {
635
+ if (error?.code === "invalid_runtime_session_handle") return null;
636
+ throw error;
637
+ }
638
+ };
639
+
640
+ const loadPersisted = async (input) => {
641
+ let persisted;
642
+ try {
643
+ persisted = await sessionStore.load(sessionKey);
644
+ } catch (error) {
645
+ if (error?.code !== "invalid_runtime_session_handle") throw error;
646
+ await sessionStore.remove(sessionKey);
647
+ return null;
648
+ }
649
+ if (!persisted) return null;
650
+ const proof = recoveryProof(persisted, input);
651
+ if (!proof) await sessionStore.remove(sessionKey);
652
+ return proof;
653
+ };
654
+
655
+ return {
656
+ async get(input) {
657
+ if (!sessionStore) {
658
+ if (
659
+ current && current.expiresAtMs - now() > minimumRemainingMs
660
+ && routeMatches(current, input)
661
+ && scopesMatch(current, input)
662
+ && (input?.expectedAgentId == null || input.expectedAgentId === current.agentId)
663
+ && (input?.expectedPrincipalId == null || input.expectedPrincipalId === current.principalId)
664
+ ) return current;
665
+ const reusableCurrent = reusable(current, input);
666
+ if (reusableCurrent) return reusableCurrent;
667
+ const previousSession = recoveryProof(current, input);
668
+ current = reusable(await issue({ ...input, previousSession }), input);
669
+ if (!current) throw new HelloLynkClientError("invalid_runtime_session_handle");
670
+ return current;
671
+ }
672
+ const inMemory = reusable(current, input);
673
+ if (inMemory) return inMemory;
674
+ current = null;
675
+ return sessionStore.withLock(sessionKey, async () => {
676
+ const persisted = await loadPersisted(input);
677
+ if (persisted && reusable(persisted, input)) {
678
+ current = persisted;
679
+ return current;
680
+ }
681
+ const issued = reusable(await issue({ ...input, previousSession: persisted }), input);
682
+ if (!issued) throw new HelloLynkClientError("invalid_runtime_session_handle");
683
+ await sessionStore.save(sessionKey, issued);
684
+ current = issued;
685
+ return current;
686
+ });
687
+ },
688
+ async clear(error) {
689
+ if (error !== undefined && !shouldInvalidateDeviceRuntimeSession(error)) return false;
690
+ const invalidatedSessionId = current?.sessionId || null;
691
+ current = null;
692
+ if (!sessionStore) return true;
693
+ return sessionStore.withLock(sessionKey, async () => {
694
+ let persisted = null;
695
+ try {
696
+ persisted = await sessionStore.load(sessionKey);
697
+ } catch (loadError) {
698
+ if (loadError?.code !== "invalid_runtime_session_handle") throw loadError;
699
+ }
700
+ if (invalidatedSessionId && persisted?.sessionId && persisted.sessionId !== invalidatedSessionId) {
701
+ return false;
702
+ }
703
+ await sessionStore.remove(sessionKey);
704
+ return true;
705
+ });
706
+ },
707
+ status() {
708
+ return current ? {
709
+ ready: current.expiresAtMs > now(),
710
+ expiresAt: new Date(current.expiresAtMs).toISOString(),
711
+ } : { ready: false, expiresAt: null };
712
+ },
713
+ };
714
+ }
715
+
716
+ export async function runSanitizedDoctor(options) {
717
+ const credentialPresent = Boolean(options?.credentialPresent);
718
+ let baseUrl;
719
+ try {
720
+ baseUrl = normalizeHelloLynkBaseUrl(options?.baseUrl, {
721
+ allowLoopbackHttp: options?.allowLoopbackHttp === true,
722
+ });
723
+ } catch (error) {
724
+ return {
725
+ ok: false,
726
+ serviceReachable: false,
727
+ credentialPresent,
728
+ platform: String(options?.platform || process.platform),
729
+ errorCode: error.code || "invalid_hellolynk_base_url",
730
+ };
731
+ }
732
+ const fetchImpl = options?.fetchImpl || runtimeSafety.helloLynkNetworkFetch;
733
+ if (typeof fetchImpl !== "function") {
734
+ return {
735
+ ok: false,
736
+ serviceReachable: false,
737
+ credentialPresent,
738
+ platform: String(options?.platform || process.platform),
739
+ errorCode: "fetch_unavailable",
740
+ };
741
+ }
742
+ try {
743
+ const { response, result } = await boundedJsonFetch(fetchImpl, `${baseUrl}/api/v1/health`, {
744
+ method: "GET",
745
+ headers: { accept: "application/json" },
746
+ redirect: "error",
747
+ }, Math.min(Math.max(Number(options?.timeoutMs) || 5_000, 1_000), 15_000));
748
+ return {
749
+ ok: response.ok && result?.ok === true && credentialPresent,
750
+ serviceReachable: response.ok,
751
+ credentialPresent,
752
+ platform: String(options?.platform || process.platform),
753
+ serviceName: typeof result?.name === "string" ? result.name : null,
754
+ securityLevel: typeof result?.securityLevel === "string" ? result.securityLevel : null,
755
+ errorCode: !response.ok || result?.ok !== true
756
+ ? "health_check_failed"
757
+ : credentialPresent
758
+ ? null
759
+ : "device_credential_unavailable",
760
+ };
761
+ } catch (error) {
762
+ return {
763
+ ok: false,
764
+ serviceReachable: false,
765
+ credentialPresent,
766
+ platform: String(options?.platform || process.platform),
767
+ errorCode: error.code || "network_request_failed",
768
+ };
769
+ }
770
+ }