@mindexec/cli 0.2.151 → 0.2.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -2125,10 +2125,13 @@ app.get('/api/local-files/:id', async (req, res) => {
2125
2125
  const httpServer = createServer(app);
2126
2126
  const wss = new WebSocketServer({ noServer: true });
2127
2127
  const remoteFrameWss = new WebSocketServer({ noServer: true });
2128
+ const remoteInputWss = new WebSocketServer({ noServer: true });
2128
2129
  const wsClients = new Set();
2129
2130
  const remoteFrameClients = new Set();
2131
+ const remoteInputClients = new Set();
2130
2132
  const shellJobs = new Map();
2131
2133
  let remoteFrameClientSeq = 0;
2134
+ let remoteInputClientSeq = 0;
2132
2135
  const remoteFrameWsDiagnostics = {
2133
2136
  opened: 0,
2134
2137
  closed: 0,
@@ -2157,6 +2160,25 @@ const remoteFrameWsDiagnostics = {
2157
2160
  lastAutoStartStarted: 0,
2158
2161
  lastAutoStartSkipped: 0
2159
2162
  };
2163
+ const remoteInputWsDiagnostics = {
2164
+ opened: 0,
2165
+ closed: 0,
2166
+ errors: 0,
2167
+ messagesReceived: 0,
2168
+ messagesInvalid: 0,
2169
+ inputsQueued: 0,
2170
+ inputsFailed: 0,
2171
+ pointerMovesQueued: 0,
2172
+ keyEventsQueued: 0,
2173
+ lastOpenAt: '',
2174
+ lastCloseAt: '',
2175
+ lastErrorAt: '',
2176
+ lastInputAt: '',
2177
+ lastDeviceId: '',
2178
+ lastInputType: '',
2179
+ lastCommandId: '',
2180
+ lastError: ''
2181
+ };
2160
2182
  const REMOTE_FRAME_WS_AUTO_START_LIMIT = 120;
2161
2183
  const REMOTE_FRAME_WS_DEFAULT_FPS = 12;
2162
2184
  const REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH = 960;
@@ -2183,6 +2205,14 @@ httpServer.on('upgrade', (req, socket, head) => {
2183
2205
  return;
2184
2206
  }
2185
2207
 
2208
+ if (parsed.pathname === '/api/remote/input/ws'
2209
+ && (!bridgeAuthRequired || token === bridgeToken)) {
2210
+ remoteInputWss.handleUpgrade(req, socket, head, (ws) => {
2211
+ remoteInputWss.emit('connection', ws, req);
2212
+ });
2213
+ return;
2214
+ }
2215
+
2186
2216
  {
2187
2217
  socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
2188
2218
  socket.destroy();
@@ -2552,6 +2582,195 @@ remoteFrameWss.on('connection', (ws, req) => {
2552
2582
  });
2553
2583
  });
2554
2584
 
2585
+ function sendRemoteInputClientJson(ws, payload) {
2586
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
2587
+ return false;
2588
+ }
2589
+
2590
+ try {
2591
+ ws.send(JSON.stringify(payload));
2592
+ return true;
2593
+ } catch {
2594
+ return false;
2595
+ }
2596
+ }
2597
+
2598
+ function normalizeRemoteInputWsPayload(payload = {}) {
2599
+ const outer = payload && typeof payload === 'object' ? payload : {};
2600
+ const inputSource = outer.input && typeof outer.input === 'object'
2601
+ ? outer.input
2602
+ : outer.Input && typeof outer.Input === 'object'
2603
+ ? outer.Input
2604
+ : outer;
2605
+ const input = { ...inputSource };
2606
+ input.type = String(input.type ?? input.Type ?? '').trim();
2607
+ const deviceId = String(
2608
+ outer.deviceId
2609
+ ?? outer.DeviceId
2610
+ ?? input.deviceId
2611
+ ?? input.DeviceId
2612
+ ?? ''
2613
+ ).trim();
2614
+ delete input.deviceId;
2615
+ delete input.DeviceId;
2616
+
2617
+ return {
2618
+ deviceId,
2619
+ input,
2620
+ requestId: String(outer.requestId ?? outer.RequestId ?? '').trim(),
2621
+ ack: outer.ack === true || outer.Ack === true
2622
+ };
2623
+ }
2624
+
2625
+ function serializeRemoteInputWsDiagnostics() {
2626
+ const clients = [];
2627
+ for (const client of remoteInputClients) {
2628
+ clients.push({
2629
+ id: client.remoteInputClientId || '',
2630
+ readyState: client.readyState,
2631
+ connectedAt: client.remoteInputConnectedAt || '',
2632
+ received: Number(client.remoteInputReceivedCount || 0),
2633
+ queued: Number(client.remoteInputQueuedCount || 0),
2634
+ failed: Number(client.remoteInputFailedCount || 0),
2635
+ bufferedAmount: Number(client.bufferedAmount || 0),
2636
+ lastInputAt: client.remoteInputLastInputAt || '',
2637
+ lastDeviceId: client.remoteInputLastDeviceId || '',
2638
+ lastInputType: client.remoteInputLastInputType || ''
2639
+ });
2640
+ }
2641
+
2642
+ return {
2643
+ protocol: 'mindexec.remote.input.json.v1',
2644
+ path: '/api/remote/input/ws',
2645
+ clientCount: remoteInputClients.size,
2646
+ ...remoteInputWsDiagnostics,
2647
+ clients
2648
+ };
2649
+ }
2650
+
2651
+ remoteInputWss.on('connection', (ws) => {
2652
+ remoteInputClients.add(ws);
2653
+ ws.remoteInputClientId = `riws-${++remoteInputClientSeq}`;
2654
+ ws.remoteInputConnectedAt = new Date().toISOString();
2655
+ ws.remoteInputReceivedCount = 0;
2656
+ ws.remoteInputQueuedCount = 0;
2657
+ ws.remoteInputFailedCount = 0;
2658
+ remoteInputWsDiagnostics.opened += 1;
2659
+ remoteInputWsDiagnostics.lastOpenAt = ws.remoteInputConnectedAt;
2660
+
2661
+ ws.on('message', data => {
2662
+ const receivedAt = new Date().toISOString();
2663
+ remoteInputWsDiagnostics.messagesReceived += 1;
2664
+ ws.remoteInputReceivedCount = (ws.remoteInputReceivedCount || 0) + 1;
2665
+
2666
+ let payload = null;
2667
+ try {
2668
+ const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
2669
+ payload = JSON.parse(text);
2670
+ } catch {
2671
+ remoteInputWsDiagnostics.messagesInvalid += 1;
2672
+ sendRemoteInputClientJson(ws, {
2673
+ type: 'RemoteInputError',
2674
+ timestamp: receivedAt,
2675
+ error: 'invalid-json'
2676
+ });
2677
+ return;
2678
+ }
2679
+
2680
+ const messageType = String(payload?.type || payload?.Type || '').trim().toLowerCase();
2681
+ if (messageType === 'ping') {
2682
+ sendRemoteInputClientJson(ws, {
2683
+ type: 'RemoteInputPong',
2684
+ timestamp: receivedAt
2685
+ });
2686
+ return;
2687
+ }
2688
+
2689
+ if (messageType !== 'input' && messageType !== 'remote.input') {
2690
+ remoteInputWsDiagnostics.messagesInvalid += 1;
2691
+ sendRemoteInputClientJson(ws, {
2692
+ type: 'RemoteInputError',
2693
+ timestamp: receivedAt,
2694
+ error: 'unsupported-message'
2695
+ });
2696
+ return;
2697
+ }
2698
+
2699
+ const normalized = normalizeRemoteInputWsPayload(payload);
2700
+ if (!normalized.deviceId || !normalized.input.type) {
2701
+ remoteInputWsDiagnostics.messagesInvalid += 1;
2702
+ sendRemoteInputClientJson(ws, {
2703
+ type: 'RemoteInputResult',
2704
+ timestamp: receivedAt,
2705
+ ok: false,
2706
+ requestId: normalized.requestId,
2707
+ deviceId: normalized.deviceId,
2708
+ inputType: normalized.input.type,
2709
+ error: !normalized.deviceId ? 'missing-device-id' : 'missing-input-type'
2710
+ });
2711
+ return;
2712
+ }
2713
+
2714
+ const result = remoteHub.sendInputControl(normalized.deviceId, normalized.input);
2715
+ const ok = result?.ok === true;
2716
+ const inputType = String(normalized.input.type || '').trim();
2717
+ remoteInputWsDiagnostics.lastInputAt = receivedAt;
2718
+ remoteInputWsDiagnostics.lastDeviceId = normalized.deviceId;
2719
+ remoteInputWsDiagnostics.lastInputType = inputType;
2720
+ remoteInputWsDiagnostics.lastCommandId = String(result?.commandId || '');
2721
+ ws.remoteInputLastInputAt = receivedAt;
2722
+ ws.remoteInputLastDeviceId = normalized.deviceId;
2723
+ ws.remoteInputLastInputType = inputType;
2724
+
2725
+ if (ok) {
2726
+ remoteInputWsDiagnostics.inputsQueued += 1;
2727
+ ws.remoteInputQueuedCount = (ws.remoteInputQueuedCount || 0) + 1;
2728
+ if (/^pointermove$/i.test(inputType)) {
2729
+ remoteInputWsDiagnostics.pointerMovesQueued += 1;
2730
+ }
2731
+ if (/^key(up|down)$/i.test(inputType)) {
2732
+ remoteInputWsDiagnostics.keyEventsQueued += 1;
2733
+ }
2734
+ } else {
2735
+ remoteInputWsDiagnostics.inputsFailed += 1;
2736
+ remoteInputWsDiagnostics.lastError = String(result?.error || 'input-failed');
2737
+ ws.remoteInputFailedCount = (ws.remoteInputFailedCount || 0) + 1;
2738
+ }
2739
+
2740
+ const shouldAck = normalized.ack === true || !ok || !/^pointermove$/i.test(inputType);
2741
+ if (shouldAck) {
2742
+ sendRemoteInputClientJson(ws, {
2743
+ type: 'RemoteInputResult',
2744
+ timestamp: receivedAt,
2745
+ ok,
2746
+ requestId: normalized.requestId,
2747
+ deviceId: normalized.deviceId,
2748
+ inputType,
2749
+ commandId: result?.commandId || '',
2750
+ error: result?.error || ''
2751
+ });
2752
+ }
2753
+ });
2754
+
2755
+ ws.on('close', () => {
2756
+ remoteInputClients.delete(ws);
2757
+ remoteInputWsDiagnostics.closed += 1;
2758
+ remoteInputWsDiagnostics.lastCloseAt = new Date().toISOString();
2759
+ });
2760
+ ws.on('error', error => {
2761
+ remoteInputClients.delete(ws);
2762
+ remoteInputWsDiagnostics.errors += 1;
2763
+ remoteInputWsDiagnostics.lastErrorAt = new Date().toISOString();
2764
+ remoteInputWsDiagnostics.lastError = String(error?.message || error || 'input-ws-error');
2765
+ });
2766
+ sendRemoteInputClientJson(ws, {
2767
+ type: 'RemoteInputSocketReady',
2768
+ timestamp: new Date().toISOString(),
2769
+ protocol: 'mindexec.remote.input.json.v1',
2770
+ clientId: ws.remoteInputClientId
2771
+ });
2772
+ });
2773
+
2555
2774
  // Helper: Validate path is within workspace
2556
2775
  function validatePath(requestedPath) {
2557
2776
  const fullPath = resolveWorkspaceBoundPath(requestedPath);
@@ -3146,6 +3365,15 @@ const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3146
3365
  const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
3147
3366
  const REMOTE_REGISTRY_REALTIME_RECONNECT_MS = Math.max(1000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS || 2500) || 2500);
3148
3367
  const REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS = Math.max(100, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS || 250) || 250);
3368
+ const REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3369
+ && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_HOST_TARGET_AUTO_RENEW || 'true'));
3370
+ const REMOTE_HOST_TARGET_LEASE_MS = Math.max(60000, Number(process.env.MINDEXEC_REMOTE_HOST_TARGET_LEASE_MS || 120000) || 120000);
3371
+ const REMOTE_HOST_TARGET_RENEW_MS = Math.max(
3372
+ 5000,
3373
+ Math.min(
3374
+ Math.floor(REMOTE_HOST_TARGET_LEASE_MS / 2),
3375
+ Number(process.env.MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS || 25000) || 25000));
3376
+ const REMOTE_HOST_TARGET_RENEW_LOG_REPEAT_MS = 60000;
3149
3377
  let remoteAgentState = createRemoteAgentIdleState();
3150
3378
  let remoteAgentSyncReportState = null;
3151
3379
  let remoteAgentSyncReportLogKey = '';
@@ -3168,6 +3396,10 @@ let remoteRegistryRealtimeHeartbeatTimer = null;
3168
3396
  let remoteRegistryRealtimeReconnectTimer = null;
3169
3397
  let remoteRegistryRealtimeReconnectContext = null;
3170
3398
  let remoteRegistryRealtimeLastWakeAt = 0;
3399
+ let remoteHostTargetRenewTimer = null;
3400
+ let remoteHostTargetRenewInFlight = false;
3401
+ let remoteHostTargetRenewLogKey = '';
3402
+ let remoteHostTargetRenewLogAt = 0;
3171
3403
  let remoteRegistryFollowerState = {
3172
3404
  enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED,
3173
3405
  status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'idle' : 'disabled',
@@ -3201,6 +3433,20 @@ let remoteRegistryRealtimeState = {
3201
3433
  changes: 0,
3202
3434
  wakeups: 0
3203
3435
  };
3436
+ let remoteHostTargetRenewState = {
3437
+ enabled: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED,
3438
+ status: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED ? 'idle' : 'disabled',
3439
+ reason: '',
3440
+ nodeId: '',
3441
+ leaseId: '',
3442
+ hostInstanceId: '',
3443
+ endpoint: '',
3444
+ endpointCandidates: [],
3445
+ expiresAt: '',
3446
+ lastAttemptAt: '',
3447
+ lastSuccessAt: '',
3448
+ lastError: ''
3449
+ };
3204
3450
 
3205
3451
  function createRemoteAgentIdleState(overrides = {}) {
3206
3452
  return {
@@ -4499,6 +4745,23 @@ function updateRemoteRegistryFollowerState(patch = {}) {
4499
4745
  emitBridgeEvent('RemoteRegistryFollowerUpdated', serializeRemoteRegistryFollowerState());
4500
4746
  }
4501
4747
 
4748
+ function serializeRemoteHostTargetRenewState() {
4749
+ return {
4750
+ ...remoteHostTargetRenewState,
4751
+ leaseMs: REMOTE_HOST_TARGET_LEASE_MS,
4752
+ renewMs: REMOTE_HOST_TARGET_RENEW_MS
4753
+ };
4754
+ }
4755
+
4756
+ function updateRemoteHostTargetRenewState(patch = {}) {
4757
+ remoteHostTargetRenewState = {
4758
+ ...remoteHostTargetRenewState,
4759
+ ...patch,
4760
+ enabled: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED
4761
+ };
4762
+ emitBridgeEvent('RemoteHostTargetRenewUpdated', serializeRemoteHostTargetRenewState());
4763
+ }
4764
+
4502
4765
  function serializeRemoteRegistryRealtimeState() {
4503
4766
  return {
4504
4767
  ...remoteRegistryRealtimeState,
@@ -5094,6 +5357,328 @@ async function fetchRemoteRegistryTarget(config, session) {
5094
5357
  return normalizeRemoteRegistryTarget(Array.isArray(payload) ? payload[0] : payload);
5095
5358
  }
5096
5359
 
5360
+ async function callRemoteRegistryRpc(config, session, functionName, payload) {
5361
+ const url = new URL(`/rest/v1/rpc/${functionName}`, config.url);
5362
+ const response = await fetch(url.toString(), {
5363
+ method: 'POST',
5364
+ headers: {
5365
+ apikey: config.key,
5366
+ Authorization: `Bearer ${session.accessToken}`,
5367
+ Accept: 'application/json',
5368
+ 'Content-Type': 'application/json'
5369
+ },
5370
+ body: JSON.stringify(payload)
5371
+ });
5372
+
5373
+ if (!response.ok) {
5374
+ const text = await response.text().catch(() => '');
5375
+ const error = new Error(`registry-rpc-${functionName}-${response.status}${text ? `:${shortenText(text, 160)}` : ''}`);
5376
+ error.statusCode = response.status;
5377
+ throw error;
5378
+ }
5379
+
5380
+ const rpcPayload = await response.json().catch(() => null);
5381
+ const row = Array.isArray(rpcPayload) ? rpcPayload[0] : rpcPayload;
5382
+ return {
5383
+ ok: row?.ok === true || row?.Ok === true,
5384
+ reason: safeRemoteAgentField(row?.reason || row?.Reason || '', 160)
5385
+ };
5386
+ }
5387
+
5388
+ async function readRemoteRegistryContext() {
5389
+ const config = readSupabaseRuntimeConfig();
5390
+ if (!config.url || !config.key) {
5391
+ return { ok: false, reason: 'supabase-config-missing' };
5392
+ }
5393
+
5394
+ const sessionPayload = await readStableAuthSessionPayload();
5395
+ const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
5396
+ if (!session) {
5397
+ return { ok: false, reason: 'registry-not-authenticated' };
5398
+ }
5399
+
5400
+ if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
5401
+ return { ok: false, reason: 'session-expired' };
5402
+ }
5403
+
5404
+ return { ok: true, config, session };
5405
+ }
5406
+
5407
+ function getRemoteHostTargetEndpointReason(endpoint) {
5408
+ const normalized = normalizeRemoteManagerEndpoint(endpoint);
5409
+ if (!normalized) {
5410
+ return 'invalid-manager-endpoint';
5411
+ }
5412
+
5413
+ const match = normalized.match(/^(\[[^\]]+\]|[^:\s]+):(\d{1,5})$/);
5414
+ const host = String(match?.[1] || '').replace(/^\[|\]$/g, '').toLowerCase();
5415
+ if (!host || host === 'localhost' || host === '::1' || host === '0:0:0:0:0:0:0:1' || /^127\./.test(host)) {
5416
+ return 'loopback-endpoint';
5417
+ }
5418
+
5419
+ if (host === '0.0.0.0' || host === '::' || host === '*') {
5420
+ return 'wildcard-endpoint';
5421
+ }
5422
+
5423
+ if (/^169\.254\./.test(host)) {
5424
+ return 'link-local-endpoint';
5425
+ }
5426
+
5427
+ return 'ok';
5428
+ }
5429
+
5430
+ function buildRemoteHostTargetRegistryPayload(hub) {
5431
+ const rawEndpointCandidates = normalizeRemoteManagerEndpointList(
5432
+ hub?.hostTargetEndpointCandidates,
5433
+ hub?.agentEndpointCandidates,
5434
+ hub?.hostTargetEndpoint,
5435
+ hub?.agentEndpoint);
5436
+ const endpointCandidates = rawEndpointCandidates
5437
+ .filter(endpoint => getRemoteHostTargetEndpointReason(endpoint) === 'ok')
5438
+ .slice(0, 12);
5439
+ const endpoint = endpointCandidates[0] || '';
5440
+ const pairToken = safeRemoteAgentField(hub?.pairToken, 512);
5441
+ const leaseId = safeRemoteAgentField(hub?.hostTargetLeaseId, 128);
5442
+ const hostInstanceId = safeRemoteAgentField(hub?.hostTargetHostInstanceId || hub?.hostInstanceId, 128);
5443
+ const nodeId = safeRemoteAgentField(hub?.hostTargetNodeId, 128);
5444
+ const activatedAt = safeRemoteAgentField(hub?.hostTargetActivatedAt, 80) || new Date().toISOString();
5445
+ const expiresAt = safeRemoteAgentField(hub?.hostTargetExpiresAt, 80) || new Date(Date.now() + REMOTE_HOST_TARGET_LEASE_MS).toISOString();
5446
+
5447
+ if (!pairToken || !leaseId || !hostInstanceId || !nodeId) {
5448
+ return {
5449
+ ok: false,
5450
+ reason: 'missing-fields',
5451
+ endpoint: '',
5452
+ endpointCandidates: []
5453
+ };
5454
+ }
5455
+
5456
+ if (endpointCandidates.length === 0) {
5457
+ const reason = rawEndpointCandidates.length === 0
5458
+ ? safeRemoteAgentField(hub?.agentEndpointRouteReason || 'missing-endpoint-candidates', 160)
5459
+ : getRemoteHostTargetEndpointReason(rawEndpointCandidates[0]);
5460
+ return {
5461
+ ok: false,
5462
+ reason,
5463
+ endpoint: rawEndpointCandidates[0] || '',
5464
+ endpointCandidates: rawEndpointCandidates
5465
+ };
5466
+ }
5467
+
5468
+ return {
5469
+ ok: true,
5470
+ endpoint,
5471
+ endpointCandidates,
5472
+ leaseId,
5473
+ hostInstanceId,
5474
+ nodeId,
5475
+ expiresAt,
5476
+ payload: {
5477
+ p_node_id: nodeId,
5478
+ p_lease_id: leaseId,
5479
+ p_host_instance_id: hostInstanceId,
5480
+ p_endpoint: endpoint,
5481
+ p_pair_token: pairToken,
5482
+ p_manager_package: safeRemoteAgentField(hub?.managerPackage || '@mindexec/cli', 160),
5483
+ p_manager_version: safeRemoteAgentField(hub?.managerVersion || '', 80),
5484
+ p_agent_package: safeRemoteAgentField(hub?.agentPackage || '@mindexec/remote', 160),
5485
+ p_activated_at: activatedAt,
5486
+ p_expires_at: expiresAt,
5487
+ p_endpoint_candidates: endpointCandidates
5488
+ }
5489
+ };
5490
+ }
5491
+
5492
+ function logRemoteHostTargetRenew(status, reason, endpoint = '') {
5493
+ const key = [status, reason, endpoint].join('|');
5494
+ const now = Date.now();
5495
+ if (key === remoteHostTargetRenewLogKey
5496
+ && now - remoteHostTargetRenewLogAt < REMOTE_HOST_TARGET_RENEW_LOG_REPEAT_MS) {
5497
+ return;
5498
+ }
5499
+
5500
+ remoteHostTargetRenewLogKey = key;
5501
+ remoteHostTargetRenewLogAt = now;
5502
+ logEvent(
5503
+ 'remote',
5504
+ `host target renew ${status} ${formatKeyValue('reason', reason || '-')} ${formatKeyValue('endpoint', endpoint || '-')}`,
5505
+ status === 'ok' ? 'remote' : 'warn');
5506
+ }
5507
+
5508
+ function isRemoteHostTargetRenewSoftSkipReason(reason) {
5509
+ return /^(registry-not-authenticated|session-expired|supabase-config-missing|missing-endpoint-candidates|loopback-endpoint|wildcard-endpoint|link-local-endpoint)$/i
5510
+ .test(String(reason || '').trim());
5511
+ }
5512
+
5513
+ async function publishLocalRemoteHostTargetToRegistry(hub, { takeover = false } = {}) {
5514
+ const registryPayload = buildRemoteHostTargetRegistryPayload(hub);
5515
+ if (!registryPayload.ok) {
5516
+ return {
5517
+ ok: false,
5518
+ active: false,
5519
+ reason: registryPayload.reason,
5520
+ endpoint: registryPayload.endpoint,
5521
+ endpointCandidates: registryPayload.endpointCandidates,
5522
+ stale: false
5523
+ };
5524
+ }
5525
+
5526
+ const context = await readRemoteRegistryContext();
5527
+ if (!context.ok) {
5528
+ return {
5529
+ ok: false,
5530
+ active: false,
5531
+ reason: context.reason,
5532
+ endpoint: registryPayload.endpoint,
5533
+ endpointCandidates: registryPayload.endpointCandidates,
5534
+ stale: false
5535
+ };
5536
+ }
5537
+
5538
+ const row = await callRemoteRegistryRpc(
5539
+ context.config,
5540
+ context.session,
5541
+ 'set_remote_host_target',
5542
+ {
5543
+ ...registryPayload.payload,
5544
+ p_takeover: takeover === true
5545
+ });
5546
+ const reason = row.reason || (row.ok ? 'ok' : 'registry-publish-failed');
5547
+ return {
5548
+ ok: row.ok,
5549
+ active: row.ok,
5550
+ reason,
5551
+ endpoint: registryPayload.endpoint,
5552
+ endpointCandidates: registryPayload.endpointCandidates,
5553
+ leaseId: registryPayload.leaseId,
5554
+ nodeId: registryPayload.nodeId,
5555
+ hostInstanceId: registryPayload.hostInstanceId,
5556
+ expiresAt: registryPayload.expiresAt,
5557
+ stale: reason === 'host-target-taken'
5558
+ };
5559
+ }
5560
+
5561
+ function scheduleRemoteHostTargetRenew(delayMs = REMOTE_HOST_TARGET_RENEW_MS, reason = 'timer') {
5562
+ if (!REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED || isShuttingDown) {
5563
+ return;
5564
+ }
5565
+
5566
+ if (remoteHostTargetRenewTimer) {
5567
+ clearTimeout(remoteHostTargetRenewTimer);
5568
+ remoteHostTargetRenewTimer = null;
5569
+ }
5570
+
5571
+ remoteHostTargetRenewTimer = setTimeout(() => {
5572
+ remoteHostTargetRenewTimer = null;
5573
+ runRemoteHostTargetRenewOnce(reason).catch(error => {
5574
+ const message = error?.message || String(error || '');
5575
+ updateRemoteHostTargetRenewState({
5576
+ status: 'error',
5577
+ reason: 'renew-error',
5578
+ lastAttemptAt: new Date().toISOString(),
5579
+ lastError: message
5580
+ });
5581
+ logRemoteHostTargetRenew('failed', message);
5582
+ scheduleRemoteHostTargetRenew(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'error-retry');
5583
+ });
5584
+ }, Math.max(0, Number(delayMs) || 0));
5585
+ remoteHostTargetRenewTimer?.unref?.();
5586
+ }
5587
+
5588
+ function stopRemoteHostTargetRenew(reason = 'stopped') {
5589
+ if (remoteHostTargetRenewTimer) {
5590
+ clearTimeout(remoteHostTargetRenewTimer);
5591
+ remoteHostTargetRenewTimer = null;
5592
+ }
5593
+ updateRemoteHostTargetRenewState({
5594
+ status: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED ? 'idle' : 'disabled',
5595
+ reason,
5596
+ nodeId: '',
5597
+ leaseId: '',
5598
+ hostInstanceId: '',
5599
+ endpoint: '',
5600
+ endpointCandidates: [],
5601
+ expiresAt: '',
5602
+ lastError: ''
5603
+ });
5604
+ }
5605
+
5606
+ async function runRemoteHostTargetRenewOnce(trigger = 'timer', options = {}) {
5607
+ if (!REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED || remoteHostTargetRenewInFlight) {
5608
+ return serializeRemoteHostTargetRenewState();
5609
+ }
5610
+
5611
+ remoteHostTargetRenewInFlight = true;
5612
+ const attemptedAt = new Date().toISOString();
5613
+ try {
5614
+ const currentHub = remoteHub.getStatus({ includeSecrets: true });
5615
+ if (currentHub?.hostTargetActive !== true || !currentHub.hostTargetNodeId) {
5616
+ stopRemoteHostTargetRenew('no-local-host-target');
5617
+ return serializeRemoteHostTargetRenewState();
5618
+ }
5619
+
5620
+ const renewed = remoteHub.setHostTarget({
5621
+ enabled: true,
5622
+ nodeId: currentHub.hostTargetNodeId,
5623
+ leaseMs: REMOTE_HOST_TARGET_LEASE_MS
5624
+ });
5625
+ if (renewed?.ok !== true || renewed?.active !== true) {
5626
+ updateRemoteHostTargetRenewState({
5627
+ status: 'error',
5628
+ reason: renewed?.error || 'local-host-renew-failed',
5629
+ nodeId: currentHub.hostTargetNodeId,
5630
+ lastAttemptAt: attemptedAt,
5631
+ lastError: renewed?.error || 'local-host-renew-failed'
5632
+ });
5633
+ scheduleRemoteHostTargetRenew(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'local-renew-failed');
5634
+ return serializeRemoteHostTargetRenewState();
5635
+ }
5636
+
5637
+ const hub = remoteHub.getStatus({ includeSecrets: true });
5638
+ const registry = await publishLocalRemoteHostTargetToRegistry(hub, {
5639
+ takeover: options.takeover === true
5640
+ });
5641
+ const status = registry.ok ? 'active' : registry.stale ? 'superseded' : 'skipped';
5642
+ updateRemoteHostTargetRenewState({
5643
+ status,
5644
+ reason: registry.reason || (registry.ok ? 'ok' : 'registry-skipped'),
5645
+ nodeId: hub.hostTargetNodeId,
5646
+ leaseId: hub.hostTargetLeaseId,
5647
+ hostInstanceId: hub.hostTargetHostInstanceId || hub.hostInstanceId,
5648
+ endpoint: registry.endpoint || hub.hostTargetEndpoint,
5649
+ endpointCandidates: registry.endpointCandidates || hub.hostTargetEndpointCandidates || [],
5650
+ expiresAt: hub.hostTargetExpiresAt,
5651
+ lastAttemptAt: attemptedAt,
5652
+ lastSuccessAt: registry.ok ? new Date().toISOString() : remoteHostTargetRenewState.lastSuccessAt,
5653
+ lastError: registry.ok || /^(registry-not-authenticated|session-expired|supabase-config-missing)$/i.test(registry.reason || '')
5654
+ ? ''
5655
+ : (registry.reason || 'registry-publish-failed')
5656
+ });
5657
+
5658
+ if (registry.stale) {
5659
+ remoteHub.setHostTarget({
5660
+ enabled: false,
5661
+ nodeId: hub.hostTargetNodeId
5662
+ });
5663
+ logRemoteHostTargetRenew('superseded', registry.reason, registry.endpoint);
5664
+ stopRemoteHostTargetRenew('host-target-superseded');
5665
+ wakeRemoteRegistryFollower('host-target-superseded').catch(() => {});
5666
+ return serializeRemoteHostTargetRenewState();
5667
+ }
5668
+
5669
+ logRemoteHostTargetRenew(registry.ok ? 'ok' : 'skipped', registry.reason, registry.endpoint);
5670
+ const retryDelay = registry.ok || isRemoteHostTargetRenewSoftSkipReason(registry.reason)
5671
+ ? REMOTE_HOST_TARGET_RENEW_MS
5672
+ : REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS;
5673
+ scheduleRemoteHostTargetRenew(
5674
+ retryDelay,
5675
+ registry.ok ? 'renewed' : 'renew-skipped');
5676
+ return serializeRemoteHostTargetRenewState();
5677
+ } finally {
5678
+ remoteHostTargetRenewInFlight = false;
5679
+ }
5680
+ }
5681
+
5097
5682
  async function reportRemoteRegistryFollowerSync(payload = {}) {
5098
5683
  const report = createRemoteAgentSyncReport(payload);
5099
5684
  rememberRemoteAgentSyncReport(report);
@@ -9913,14 +10498,17 @@ app.get('/api/status', async (req, res) => {
9913
10498
  wsToken,
9914
10499
  wsPath: '/events',
9915
10500
  remoteFrameWsPath: '/api/remote/frames/ws',
10501
+ remoteInputWsPath: '/api/remote/input/ws',
9916
10502
  bridgeToken,
9917
10503
  bridgeTokenHeader,
9918
10504
  bridgeAuthRequired,
9919
10505
  remoteHub: remoteHub.getStatus({ includeSecrets: false }),
9920
10506
  remoteFrameWs: serializeRemoteFrameWsDiagnostics(),
10507
+ remoteInputWs: serializeRemoteInputWsDiagnostics(),
9921
10508
  remoteAgent: serializeRemoteAgentState(),
9922
10509
  remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
9923
10510
  remoteRegistryRealtime: serializeRemoteRegistryRealtimeState(),
10511
+ remoteHostTargetRenew: serializeRemoteHostTargetRenewState(),
9924
10512
  shellJobsPath: '/api/shell/jobs',
9925
10513
  companyCore: {
9926
10514
  baseUrl: companyCoreBaseUrl,
@@ -9997,20 +10585,27 @@ app.post('/api/remote/frames', (req, res) => {
9997
10585
  app.post('/api/remote/host-target', async (req, res) => {
9998
10586
  res.setHeader('Cache-Control', 'no-store');
9999
10587
  try {
10588
+ const requestedLeaseMs = Number(req.body?.leaseMs);
10000
10589
  const result = remoteHub.setHostTarget({
10001
10590
  enabled: req.body?.enabled !== false,
10002
10591
  nodeId: req.body?.nodeId,
10003
- leaseMs: req.body?.leaseMs
10592
+ leaseMs: Math.max(
10593
+ REMOTE_HOST_TARGET_LEASE_MS,
10594
+ Number.isFinite(requestedLeaseMs) && requestedLeaseMs > 0 ? requestedLeaseMs : 0)
10004
10595
  });
10005
10596
  if (result?.ok === true && result?.active === true && isLocalRemoteHostTargetActive()) {
10006
10597
  if (isRemoteAgentProcessRunning()) {
10007
10598
  await stopRemoteAgentConnection('local-host-target-active');
10008
10599
  logEvent('remote', 'managed RemoteAgent held stopped while local host target is active', 'remote');
10009
10600
  }
10601
+ await runRemoteHostTargetRenewOnce('set-host', { takeover: true });
10602
+ } else if (result?.active !== true) {
10603
+ stopRemoteHostTargetRenew('host-target-inactive');
10010
10604
  }
10011
10605
  res.json({
10012
10606
  ...result,
10013
- agent: serializeRemoteAgentState()
10607
+ agent: serializeRemoteAgentState(),
10608
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10014
10609
  });
10015
10610
  } catch (err) {
10016
10611
  logError('remote', 'remote host target update failed.', err);
@@ -10018,17 +10613,25 @@ app.post('/api/remote/host-target', async (req, res) => {
10018
10613
  ok: false,
10019
10614
  active: false,
10020
10615
  error: err?.message || String(err),
10021
- agent: serializeRemoteAgentState()
10616
+ agent: serializeRemoteAgentState(),
10617
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10022
10618
  });
10023
10619
  }
10024
10620
  });
10025
10621
 
10026
10622
  app.delete('/api/remote/host-target', (req, res) => {
10027
10623
  res.setHeader('Cache-Control', 'no-store');
10028
- res.json(remoteHub.setHostTarget({
10624
+ const result = remoteHub.setHostTarget({
10029
10625
  enabled: false,
10030
10626
  nodeId: req.body?.nodeId
10031
- }));
10627
+ });
10628
+ if (result?.ok === true) {
10629
+ stopRemoteHostTargetRenew('host-target-cleared');
10630
+ }
10631
+ res.json({
10632
+ ...result,
10633
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10634
+ });
10032
10635
  });
10033
10636
 
10034
10637
  app.get('/api/remote/agent/status', (req, res) => {
@@ -11745,12 +12348,46 @@ async function shutdownBridge(signal) {
11745
12348
  // Ignore if already closed
11746
12349
  }
11747
12350
 
12351
+ for (const client of remoteFrameClients) {
12352
+ try {
12353
+ client.close();
12354
+ } catch {
12355
+ // Ignore close race
12356
+ }
12357
+ }
12358
+
12359
+ for (const client of remoteInputClients) {
12360
+ try {
12361
+ client.close();
12362
+ } catch {
12363
+ // Ignore close race
12364
+ }
12365
+ }
12366
+
12367
+ try {
12368
+ remoteFrameWss.close();
12369
+ } catch {
12370
+ // Ignore if already closed
12371
+ }
12372
+
12373
+ try {
12374
+ remoteInputWss.close();
12375
+ } catch {
12376
+ // Ignore if already closed
12377
+ }
12378
+
11748
12379
  try {
11749
12380
  stopRemoteRegistryFollower();
11750
12381
  } catch {
11751
12382
  // Ignore registry follower shutdown errors
11752
12383
  }
11753
12384
 
12385
+ try {
12386
+ stopRemoteHostTargetRenew('bridge-shutdown');
12387
+ } catch {
12388
+ // Ignore host-target renew shutdown errors
12389
+ }
12390
+
11754
12391
  try {
11755
12392
  closeRemoteRegistryRealtime('bridge-shutdown');
11756
12393
  } catch {