@mindexec/cli 0.2.185 → 0.2.187

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.185",
3
+ "version": "0.2.187",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -15,6 +15,9 @@ const PAIR_TOKEN = 'remote-registry-follower-pair-token';
15
15
  const USER_ID = '11111111-2222-4333-8444-555555555555';
16
16
  const SUPABASE_KEY = 'remote-registry-follower-key';
17
17
  const ACCESS_TOKEN = 'remote-registry-follower-access-token';
18
+ const EXPIRED_ACCESS_TOKEN = 'remote-registry-follower-expired-access-token';
19
+ const REFRESHED_ACCESS_TOKEN = 'remote-registry-follower-refreshed-access-token';
20
+ const REFRESH_TOKEN = 'remote-registry-follower-refresh-token';
18
21
  const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
22
 
20
23
  function wait(ms) {
@@ -86,6 +89,14 @@ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active =
86
89
  function startFakeSupabase(getTarget, setTarget = null) {
87
90
  const requests = [];
88
91
  const realtimeClients = new Set();
92
+ const validAccessTokens = new Set([ACCESS_TOKEN]);
93
+ const assertRegistryAuth = req => {
94
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
95
+ const authorization = String(req.headers.authorization || '');
96
+ assert.ok(
97
+ validAccessTokens.has(authorization.replace(/^Bearer\s+/i, '')),
98
+ `unexpected registry authorization: ${authorization}`);
99
+ };
89
100
  const server = createServer(async (req, res) => {
90
101
  const readBody = () => new Promise(resolve => {
91
102
  let body = '';
@@ -103,8 +114,7 @@ function startFakeSupabase(getTarget, setTarget = null) {
103
114
  });
104
115
 
105
116
  if (req.method === 'GET' && parsed.pathname === '/rest/v1/remote_host_targets') {
106
- assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
107
- assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
117
+ assertRegistryAuth(req);
108
118
  res.writeHead(200, {
109
119
  'Content-Type': 'application/json',
110
120
  'Cache-Control': 'no-store'
@@ -115,8 +125,7 @@ function startFakeSupabase(getTarget, setTarget = null) {
115
125
  }
116
126
 
117
127
  if (req.method === 'POST' && parsed.pathname === '/rest/v1/rpc/set_remote_host_target') {
118
- assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
119
- assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
128
+ assertRegistryAuth(req);
120
129
  const body = JSON.parse(await readBody() || '{}');
121
130
  const now = Date.now();
122
131
  const existing = getTarget();
@@ -148,6 +157,31 @@ function startFakeSupabase(getTarget, setTarget = null) {
148
157
  return;
149
158
  }
150
159
 
160
+ if (req.method === 'POST' && parsed.pathname === '/auth/v1/token') {
161
+ assert.equal(parsed.searchParams.get('grant_type'), 'refresh_token');
162
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
163
+ assert.equal(String(req.headers.authorization || ''), `Bearer ${SUPABASE_KEY}`);
164
+ const body = JSON.parse(await readBody() || '{}');
165
+ assert.equal(body.refresh_token, REFRESH_TOKEN);
166
+ validAccessTokens.add(REFRESHED_ACCESS_TOKEN);
167
+ res.writeHead(200, {
168
+ 'Content-Type': 'application/json',
169
+ 'Cache-Control': 'no-store'
170
+ });
171
+ res.end(JSON.stringify({
172
+ access_token: REFRESHED_ACCESS_TOKEN,
173
+ refresh_token: REFRESH_TOKEN,
174
+ token_type: 'bearer',
175
+ expires_in: 3600,
176
+ expires_at: Math.floor(Date.now() / 1000) + 3600,
177
+ user: {
178
+ id: USER_ID,
179
+ email: 'remote-registry-follower@example.test'
180
+ }
181
+ }));
182
+ return;
183
+ }
184
+
151
185
  res.writeHead(404, { 'Content-Type': 'application/json' });
152
186
  res.end(JSON.stringify({ error: 'not-found' }));
153
187
  });
@@ -166,8 +200,8 @@ function startFakeSupabase(getTarget, setTarget = null) {
166
200
 
167
201
  if (message.event === 'phx_join') {
168
202
  assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
169
- assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
170
- assert.equal(message.payload?.access_token, ACCESS_TOKEN);
203
+ assert.ok(validAccessTokens.has(String(req.headers.authorization || '').replace(/^Bearer\s+/i, '')));
204
+ assert.ok(validAccessTokens.has(String(message.payload?.access_token || '')));
171
205
  assert.deepEqual(
172
206
  message.payload?.config?.postgres_changes?.[0],
173
207
  {
@@ -338,10 +372,14 @@ async function waitForBridge(bridge) {
338
372
  }, 30000, `bridge startup\n${bridge.details()}`);
339
373
  }
340
374
 
341
- function createSessionPayload() {
375
+ function createSessionPayload(options = {}) {
376
+ const accessToken = options.accessToken || ACCESS_TOKEN;
377
+ const refreshToken = options.refreshToken || REFRESH_TOKEN;
378
+ const expiresAt = options.expiresAt ?? Math.floor(Date.now() / 1000) + 3600;
342
379
  return JSON.stringify({
343
- access_token: ACCESS_TOKEN,
344
- expires_at: Math.floor(Date.now() / 1000) + 3600,
380
+ access_token: accessToken,
381
+ refresh_token: refreshToken,
382
+ expires_at: expiresAt,
345
383
  user: {
346
384
  id: USER_ID,
347
385
  email: 'remote-registry-follower@example.test'
@@ -349,9 +387,9 @@ function createSessionPayload() {
349
387
  });
350
388
  }
351
389
 
352
- async function writeSession(authRoot) {
390
+ async function writeSession(authRoot, options = {}) {
353
391
  await mkdir(authRoot, { recursive: true });
354
- await writeFile(path.join(authRoot, 'supabase-session.json'), createSessionPayload(), 'utf8');
392
+ await writeFile(path.join(authRoot, 'supabase-session.json'), createSessionPayload(options), 'utf8');
355
393
  }
356
394
 
357
395
  async function waitForManagedAgent(bridge, managerEndpoint, label, timeoutMs = 20000) {
@@ -435,7 +473,10 @@ async function main() {
435
473
  const hostBAuth = path.join(tempRoot, 'auth-host-b');
436
474
  const clientAuth = path.join(tempRoot, 'auth-client');
437
475
  await writeSession(renewHostAuth);
438
- await writeSession(clientAuth);
476
+ await writeSession(clientAuth, {
477
+ accessToken: EXPIRED_ACCESS_TOKEN,
478
+ expiresAt: Math.floor(Date.now() / 1000) - 30
479
+ });
439
480
 
440
481
  renewHost = spawnBridge({
441
482
  bridgePort: renewHostBridgePort,
@@ -544,6 +585,17 @@ async function main() {
544
585
  assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
545
586
  assert.equal(firstAgent.manager, hostAEndpoint);
546
587
  assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
588
+ assert.ok(
589
+ fakeSupabase.requests.some(request =>
590
+ request.method === 'POST'
591
+ && request.pathname === '/auth/v1/token'),
592
+ 'expected LocalBridge to refresh expired persisted Supabase session without browser involvement');
593
+ assert.ok(
594
+ fakeSupabase.requests.some(request =>
595
+ request.method === 'GET'
596
+ && request.pathname === '/rest/v1/remote_host_targets'
597
+ && request.authorization === `Bearer ${REFRESHED_ACCESS_TOKEN}`),
598
+ 'expected registry read to use refreshed access token');
547
599
  await waitForConnectedDevice(hostA, 'host-a');
548
600
  const firstStatus = await fetchJson(`${client.baseUrl}/api/status`);
549
601
  assert.equal(firstStatus.payload?.remoteRegistryRealtime?.subscribed, true, JSON.stringify(firstStatus.payload?.remoteRegistryRealtime));
@@ -649,12 +701,18 @@ async function main() {
649
701
  fakeSupabase.broadcastChange('UPDATE');
650
702
 
651
703
  await waitFor(async () => {
652
- const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
653
- return result.payload?.running === false
654
- && String(result.payload?.lastError || '') === 'registry-inactive'
655
- ? result.payload
704
+ const status = await fetchJson(`${client.baseUrl}/api/status`);
705
+ const agent = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
706
+ return status.payload?.remoteRegistryFollower?.reason === 'registry-inactive-agent-kept'
707
+ && status.payload?.remoteRegistryFollower?.agentKept === true
708
+ && status.payload?.remoteRegistryFollower?.dataPlaneReady === true
709
+ && agent.payload?.running === true
710
+ && agent.payload?.ready === true
711
+ && String(agent.payload?.manager || '') === hostBEndpoint
712
+ && Number(agent.payload?.pid || 0) === Number(restartedAgent.pid || 0)
713
+ ? { status: status.payload.remoteRegistryFollower, agent: agent.payload }
656
714
  : null;
657
- }, 9000, `registry inactive stop\n${client.details()}`);
715
+ }, 9000, `registry inactive keeps ready data-plane agent\n${client.details()}`);
658
716
  const finalStatus = await fetchJson(`${client.baseUrl}/api/status`);
659
717
  assert.ok(finalStatus.payload?.remoteRegistryRealtime?.changes >= 2, JSON.stringify(finalStatus.payload?.remoteRegistryRealtime));
660
718
  assert.ok(finalStatus.payload?.remoteRegistryRealtime?.wakeups >= 2, JSON.stringify(finalStatus.payload?.remoteRegistryRealtime));
package/server.js CHANGED
@@ -3388,6 +3388,7 @@ const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_FAST_RETRY_COUNT = Math.max(
3388
3388
  Number(process.env.MINDEXEC_REMOTE_REGISTRY_MISSING_SESSION_FAST_RETRY_COUNT || 5) || 5);
3389
3389
  const REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT = Math.max(2, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_STOP_COUNT || 8) || 8);
3390
3390
  const REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_GRACE_MS || 60000) || 60000);
3391
+ const REMOTE_REGISTRY_SESSION_REFRESH_LEAD_MS = Math.max(15_000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_SESSION_REFRESH_LEAD_MS || 120_000) || 120_000);
3391
3392
  const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3392
3393
  && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME || 'true'));
3393
3394
  const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
@@ -3426,6 +3427,7 @@ let remoteRegistryFollowerStarted = false;
3426
3427
  let remoteRegistryFollowerConsecutiveMissingSession = 0;
3427
3428
  let remoteRegistryFollowerConsecutiveInactiveTarget = 0;
3428
3429
  let remoteRegistryFollowerInactiveTargetFirstAt = 0;
3430
+ let remoteRegistrySessionRefreshPromise = null;
3429
3431
  let remoteRegistryRealtimeSocket = null;
3430
3432
  let remoteRegistryRealtimeSessionKey = '';
3431
3433
  let remoteRegistryRealtimeTopic = '';
@@ -3573,6 +3575,7 @@ function createRemoteAgentSyncReport(body = {}) {
3573
3575
  inactiveCount: Number(body.inactiveCount ?? body.InactiveCount ?? 0) || 0,
3574
3576
  inactiveElapsedMs: Number(body.inactiveElapsedMs ?? body.InactiveElapsedMs ?? 0) || 0,
3575
3577
  agentKept: body.agentKept === true || body.AgentKept === true,
3578
+ dataPlaneReady: body.dataPlaneReady === true || body.DataPlaneReady === true,
3576
3579
  updatedAt: new Date().toISOString()
3577
3580
  };
3578
3581
  }
@@ -4089,6 +4092,12 @@ function isRemoteAgentRunningButNotReadyStale() {
4089
4092
  && getRemoteAgentRunningAgeMs(remoteAgentState) >= REMOTE_AGENT_READY_STALE_MS;
4090
4093
  }
4091
4094
 
4095
+ function isRemoteAgentReadyDataPlaneAlive() {
4096
+ return isRemoteAgentRegistryOwned()
4097
+ && remoteAgentState.ready === true
4098
+ && remoteAgentState.needsReconnect !== true;
4099
+ }
4100
+
4092
4101
  function isRemoteAgentRegistryOwned() {
4093
4102
  return isRemoteAgentProcessRunning()
4094
4103
  && /registry/i.test(String(remoteAgentState.source || ''));
@@ -4138,13 +4147,15 @@ function rememberRemoteRegistryInactiveTarget(reason = 'no-active-target') {
4138
4147
 
4139
4148
  const elapsedMs = Math.max(0, now - remoteRegistryFollowerInactiveTargetFirstAt);
4140
4149
  const agentRunning = isRemoteAgentRegistryOwned();
4141
- const confirmedInactive = remoteRegistryFollowerConsecutiveInactiveTarget >= REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT
4150
+ const keepReadyDataPlane = agentRunning && isRemoteAgentReadyDataPlaneAlive();
4151
+ const confirmedInactive = !keepReadyDataPlane
4152
+ && remoteRegistryFollowerConsecutiveInactiveTarget >= REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT
4142
4153
  && elapsedMs >= REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS;
4143
4154
 
4144
4155
  if (agentRunning && !confirmedInactive) {
4145
4156
  logEvent(
4146
4157
  'remote',
4147
- `registry target temporarily inactive; keeping managed RemoteAgent ${formatKeyValue('reason', reason)} ${formatKeyValue('count', remoteRegistryFollowerConsecutiveInactiveTarget)} ${formatKeyValue('elapsedMs', elapsedMs)}`,
4158
+ `registry target temporarily inactive; keeping managed RemoteAgent ${formatKeyValue('reason', reason)} ${formatKeyValue('count', remoteRegistryFollowerConsecutiveInactiveTarget)} ${formatKeyValue('elapsedMs', elapsedMs)} ${formatKeyValue('dataPlane', keepReadyDataPlane ? 'ready' : 'pending')}`,
4148
4159
  'remote');
4149
4160
  }
4150
4161
 
@@ -4153,7 +4164,8 @@ function rememberRemoteRegistryInactiveTarget(reason = 'no-active-target') {
4153
4164
  elapsedMs,
4154
4165
  agentRunning,
4155
4166
  confirmedInactive,
4156
- agentKept: agentRunning && !confirmedInactive
4167
+ agentKept: agentRunning && !confirmedInactive,
4168
+ dataPlaneReady: keepReadyDataPlane
4157
4169
  };
4158
4170
  }
4159
4171
 
@@ -5523,6 +5535,75 @@ function readSupabaseSessionField(source, ...keys) {
5523
5535
  return '';
5524
5536
  }
5525
5537
 
5538
+ function parseSupabaseSessionExpiresAtMs(value) {
5539
+ if (value === undefined || value === null || String(value).trim() === '') {
5540
+ return 0;
5541
+ }
5542
+
5543
+ const numeric = Number(value);
5544
+ if (Number.isFinite(numeric) && numeric > 0) {
5545
+ return numeric > 9999999999 ? numeric : numeric * 1000;
5546
+ }
5547
+
5548
+ const parsed = Date.parse(String(value));
5549
+ return Number.isFinite(parsed) ? parsed : 0;
5550
+ }
5551
+
5552
+ function setSupabaseSessionKnownField(session, value, fallbackKey, ...keys) {
5553
+ let wrote = false;
5554
+ for (const key of keys) {
5555
+ if (Object.prototype.hasOwnProperty.call(session, key)) {
5556
+ session[key] = value;
5557
+ wrote = true;
5558
+ }
5559
+ }
5560
+
5561
+ if (!wrote) {
5562
+ session[fallbackKey] = value;
5563
+ }
5564
+ }
5565
+
5566
+ function buildRefreshedSupabaseSessionPayload(content, refreshedSession) {
5567
+ const existing = JSON.parse(String(content || '{}'));
5568
+ const accessToken = String(readSupabaseSessionField(refreshedSession, 'access_token', 'accessToken', 'AccessToken')).trim();
5569
+ const refreshToken = String(readSupabaseSessionField(refreshedSession, 'refresh_token', 'refreshToken', 'RefreshToken')).trim()
5570
+ || String(readSupabaseSessionField(existing, 'refresh_token', 'refreshToken', 'RefreshToken')).trim();
5571
+ const tokenType = String(readSupabaseSessionField(refreshedSession, 'token_type', 'tokenType', 'TokenType')).trim()
5572
+ || String(readSupabaseSessionField(existing, 'token_type', 'tokenType', 'TokenType')).trim()
5573
+ || 'bearer';
5574
+ const expiresInRaw = Number(readSupabaseSessionField(refreshedSession, 'expires_in', 'expiresIn', 'ExpiresIn') || 0);
5575
+ const expiresAtFromResponse = readSupabaseSessionField(refreshedSession, 'expires_at', 'expiresAt', 'ExpiresAt');
5576
+ const expiresAtMs = parseSupabaseSessionExpiresAtMs(expiresAtFromResponse)
5577
+ || (Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? Date.now() + (expiresInRaw * 1000) : 0);
5578
+ const expiresAtSeconds = expiresAtMs > 0 ? Math.floor(expiresAtMs / 1000) : 0;
5579
+
5580
+ if (!accessToken || !refreshToken || !expiresAtSeconds) {
5581
+ const error = new Error('auth-refresh-response-incomplete');
5582
+ error.statusCode = 502;
5583
+ throw error;
5584
+ }
5585
+
5586
+ setSupabaseSessionKnownField(existing, accessToken, 'access_token', 'access_token', 'accessToken', 'AccessToken');
5587
+ setSupabaseSessionKnownField(existing, refreshToken, 'refresh_token', 'refresh_token', 'refreshToken', 'RefreshToken');
5588
+ setSupabaseSessionKnownField(existing, tokenType, 'token_type', 'token_type', 'tokenType', 'TokenType');
5589
+ setSupabaseSessionKnownField(existing, expiresAtSeconds, 'expires_at', 'expires_at', 'expiresAt', 'ExpiresAt');
5590
+
5591
+ if (Number.isFinite(expiresInRaw) && expiresInRaw > 0) {
5592
+ setSupabaseSessionKnownField(existing, Math.floor(expiresInRaw), 'expires_in', 'expires_in', 'expiresIn', 'ExpiresIn');
5593
+ }
5594
+
5595
+ const user = refreshedSession?.user || refreshedSession?.User;
5596
+ if (user && typeof user === 'object') {
5597
+ if (Object.prototype.hasOwnProperty.call(existing, 'User')) {
5598
+ existing.User = user;
5599
+ } else {
5600
+ existing.user = user;
5601
+ }
5602
+ }
5603
+
5604
+ return JSON.stringify(existing);
5605
+ }
5606
+
5526
5607
  function parseSupabaseSessionForRegistry(content) {
5527
5608
  let session = null;
5528
5609
  try {
@@ -5533,13 +5614,11 @@ function parseSupabaseSessionForRegistry(content) {
5533
5614
 
5534
5615
  const user = session?.user || session?.User || {};
5535
5616
  const accessToken = String(readSupabaseSessionField(session, 'access_token', 'accessToken', 'AccessToken')).trim();
5617
+ const refreshToken = String(readSupabaseSessionField(session, 'refresh_token', 'refreshToken', 'RefreshToken')).trim();
5536
5618
  const userId = String(
5537
5619
  readSupabaseSessionField(user, 'id', 'Id')
5538
5620
  || readSupabaseSessionField(session, 'user_id', 'userId', 'UserId')).trim();
5539
- const expiresAtRaw = Number(readSupabaseSessionField(session, 'expires_at', 'expiresAt', 'ExpiresAt') || 0);
5540
- const expiresAtMs = Number.isFinite(expiresAtRaw) && expiresAtRaw > 0
5541
- ? (expiresAtRaw > 9999999999 ? expiresAtRaw : expiresAtRaw * 1000)
5542
- : 0;
5621
+ const expiresAtMs = parseSupabaseSessionExpiresAtMs(readSupabaseSessionField(session, 'expires_at', 'expiresAt', 'ExpiresAt'));
5543
5622
 
5544
5623
  if (!accessToken || !userId) {
5545
5624
  return null;
@@ -5547,11 +5626,143 @@ function parseSupabaseSessionForRegistry(content) {
5547
5626
 
5548
5627
  return {
5549
5628
  accessToken,
5629
+ refreshToken,
5550
5630
  userId,
5551
5631
  expiresAtMs
5552
5632
  };
5553
5633
  }
5554
5634
 
5635
+ async function refreshSupabaseSessionForRegistry(config, sessionPayload, reason = 'registry') {
5636
+ const currentSession = parseSupabaseSessionForRegistry(sessionPayload?.content);
5637
+ if (!currentSession?.refreshToken) {
5638
+ return {
5639
+ ok: false,
5640
+ reason: 'refresh-token-missing'
5641
+ };
5642
+ }
5643
+
5644
+ if (remoteRegistrySessionRefreshPromise) {
5645
+ return await remoteRegistrySessionRefreshPromise;
5646
+ }
5647
+
5648
+ remoteRegistrySessionRefreshPromise = (async () => {
5649
+ const url = new URL('/auth/v1/token', config.url);
5650
+ url.searchParams.set('grant_type', 'refresh_token');
5651
+ const response = await fetch(url.toString(), {
5652
+ method: 'POST',
5653
+ headers: {
5654
+ apikey: config.key,
5655
+ Authorization: `Bearer ${config.key}`,
5656
+ Accept: 'application/json',
5657
+ 'Content-Type': 'application/json'
5658
+ },
5659
+ body: JSON.stringify({
5660
+ refresh_token: currentSession.refreshToken
5661
+ })
5662
+ });
5663
+
5664
+ if (!response.ok) {
5665
+ const text = await response.text().catch(() => '');
5666
+ const error = new Error(`auth-refresh-${response.status}${text ? `:${shortenText(text, 160)}` : ''}`);
5667
+ error.statusCode = response.status;
5668
+ throw error;
5669
+ }
5670
+
5671
+ const refreshed = await response.json();
5672
+ const content = buildRefreshedSupabaseSessionPayload(sessionPayload.content, refreshed);
5673
+ await writeStableAuthSessionPayload(content);
5674
+ const session = parseSupabaseSessionForRegistry(content);
5675
+ if (!session) {
5676
+ const error = new Error('auth-refresh-session-parse-failed');
5677
+ error.statusCode = 502;
5678
+ throw error;
5679
+ }
5680
+
5681
+ logEvent(
5682
+ 'remote',
5683
+ `registry auth refreshed ${formatKeyValue('reason', reason || 'registry')} ${formatKeyValue('expiresInMs', Math.max(0, session.expiresAtMs - Date.now()))}`,
5684
+ 'remote');
5685
+
5686
+ return {
5687
+ ok: true,
5688
+ reason: 'refreshed',
5689
+ content,
5690
+ session
5691
+ };
5692
+ })();
5693
+
5694
+ try {
5695
+ return await remoteRegistrySessionRefreshPromise;
5696
+ } finally {
5697
+ remoteRegistrySessionRefreshPromise = null;
5698
+ }
5699
+ }
5700
+
5701
+ async function readFreshSupabaseSessionForRegistry(config, reason = 'registry') {
5702
+ const sessionPayload = await readStableAuthSessionPayload();
5703
+ const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
5704
+ if (!session) {
5705
+ return {
5706
+ found: sessionPayload.found,
5707
+ payload: sessionPayload,
5708
+ session: null,
5709
+ refreshed: false,
5710
+ expired: false
5711
+ };
5712
+ }
5713
+
5714
+ const now = Date.now();
5715
+ const shouldRefresh = session.expiresAtMs > 0
5716
+ && session.expiresAtMs <= now + REMOTE_REGISTRY_SESSION_REFRESH_LEAD_MS;
5717
+ if (!shouldRefresh) {
5718
+ return {
5719
+ found: true,
5720
+ payload: sessionPayload,
5721
+ session,
5722
+ refreshed: false,
5723
+ expired: session.expiresAtMs > 0 && session.expiresAtMs <= now
5724
+ };
5725
+ }
5726
+
5727
+ try {
5728
+ const refreshed = await refreshSupabaseSessionForRegistry(config, sessionPayload, reason);
5729
+ if (refreshed.ok && refreshed.session) {
5730
+ return {
5731
+ found: true,
5732
+ payload: {
5733
+ ...sessionPayload,
5734
+ content: refreshed.content,
5735
+ refreshed: true
5736
+ },
5737
+ session: refreshed.session,
5738
+ refreshed: true,
5739
+ expired: false
5740
+ };
5741
+ }
5742
+
5743
+ return {
5744
+ found: true,
5745
+ payload: sessionPayload,
5746
+ session,
5747
+ refreshed: false,
5748
+ expired: session.expiresAtMs > 0 && session.expiresAtMs <= Date.now(),
5749
+ refreshError: refreshed.reason || 'auth-refresh-skipped'
5750
+ };
5751
+ } catch (error) {
5752
+ logWarn(
5753
+ 'remote',
5754
+ `registry auth refresh failed ${formatKeyValue('reason', reason || 'registry')} ${formatKeyValue('error', error?.message || String(error || ''))}`);
5755
+ return {
5756
+ found: true,
5757
+ payload: sessionPayload,
5758
+ session,
5759
+ refreshed: false,
5760
+ expired: session.expiresAtMs > 0 && session.expiresAtMs <= Date.now(),
5761
+ refreshError: error?.message || String(error || '')
5762
+ };
5763
+ }
5764
+ }
5765
+
5555
5766
  function readRegistryTargetField(target, ...keys) {
5556
5767
  for (const key of keys) {
5557
5768
  const value = target?.[key];
@@ -5690,13 +5901,13 @@ async function readRemoteRegistryContext() {
5690
5901
  return { ok: false, reason: 'supabase-config-missing' };
5691
5902
  }
5692
5903
 
5693
- const sessionPayload = await readStableAuthSessionPayload();
5694
- const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
5904
+ const sessionState = await readFreshSupabaseSessionForRegistry(config, 'registry-context');
5905
+ const session = sessionState.session;
5695
5906
  if (!session) {
5696
5907
  return { ok: false, reason: 'registry-not-authenticated' };
5697
5908
  }
5698
5909
 
5699
- if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
5910
+ if (sessionState.expired || (session.expiresAtMs && session.expiresAtMs <= Date.now())) {
5700
5911
  return { ok: false, reason: 'session-expired' };
5701
5912
  }
5702
5913
 
@@ -6311,8 +6522,8 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6311
6522
  return serializeRemoteRegistryFollowerState();
6312
6523
  }
6313
6524
 
6314
- const sessionPayload = await readStableAuthSessionPayload();
6315
- const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
6525
+ const sessionState = await readFreshSupabaseSessionForRegistry(config, `follower-${trigger || 'timer'}`);
6526
+ const session = sessionState.session;
6316
6527
  if (!session) {
6317
6528
  closeRemoteRegistryRealtime('registry-not-authenticated');
6318
6529
  remoteRegistryFollowerConsecutiveMissingSession += 1;
@@ -6358,23 +6569,39 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6358
6569
  }
6359
6570
 
6360
6571
  remoteRegistryFollowerConsecutiveMissingSession = 0;
6361
- if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
6572
+ if (sessionState.expired || (session.expiresAtMs && session.expiresAtMs <= Date.now())) {
6362
6573
  closeRemoteRegistryRealtime('session-expired');
6574
+ const agentKept = isRemoteAgentRegistryOwned() && isRemoteAgentProcessRunning();
6575
+ const reason = agentKept
6576
+ ? 'session-expired-agent-kept'
6577
+ : 'session-expired';
6578
+ const keptManager = agentKept ? safeRemoteAgentField(remoteAgentState.manager, 160) : '';
6579
+ const keptCandidates = agentKept ? normalizeRemoteManagerEndpointList(remoteAgentState.managerCandidates, remoteAgentState.manager) : [];
6363
6580
  updateRemoteRegistryFollowerState({
6364
- status: 'skipped',
6365
- reason: 'session-expired',
6581
+ status: agentKept ? 'auth-pending' : 'skipped',
6582
+ reason,
6366
6583
  authenticated: false,
6367
6584
  lastAttemptAt: attemptedAt,
6368
- lastError: ''
6585
+ lastError: sessionState.refreshError || '',
6586
+ targetEndpoint: keptManager,
6587
+ targetEndpointCandidates: keptCandidates,
6588
+ targetLeaseId: agentKept ? safeRemoteAgentField(remoteAgentState.leaseId, 128) : '',
6589
+ targetNodeId: agentKept ? safeRemoteAgentField(remoteAgentState.nodeId, 128) : '',
6590
+ agentKept
6369
6591
  });
6370
6592
  await reportRemoteRegistryFollowerSync({
6371
6593
  ok: true,
6372
6594
  skipped: true,
6373
- reason: 'session-expired',
6595
+ reason,
6374
6596
  trigger,
6375
- authenticated: false
6597
+ authenticated: false,
6598
+ targetEndpoint: keptManager,
6599
+ targetEndpointCandidates: keptCandidates,
6600
+ targetLeaseId: agentKept ? remoteAgentState.leaseId : '',
6601
+ targetNodeId: agentKept ? remoteAgentState.nodeId : '',
6602
+ agentKept
6376
6603
  });
6377
- scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'session-expired');
6604
+ scheduleRemoteRegistryFollower(agentKept ? REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS : REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'session-expired');
6378
6605
  return serializeRemoteRegistryFollowerState();
6379
6606
  }
6380
6607
 
@@ -6470,6 +6697,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6470
6697
  inactiveCount: inactive.count,
6471
6698
  inactiveElapsedMs: inactive.elapsedMs,
6472
6699
  agentKept: inactive.agentKept,
6700
+ dataPlaneReady: inactive.dataPlaneReady,
6473
6701
  lastError: ''
6474
6702
  });
6475
6703
  await reportRemoteRegistryFollowerSync({
@@ -6484,7 +6712,8 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6484
6712
  targetNodeId: inactive.agentKept ? remoteAgentState.nodeId : '',
6485
6713
  inactiveCount: inactive.count,
6486
6714
  inactiveElapsedMs: inactive.elapsedMs,
6487
- agentKept: inactive.agentKept
6715
+ agentKept: inactive.agentKept,
6716
+ dataPlaneReady: inactive.dataPlaneReady
6488
6717
  });
6489
6718
  scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'no-target');
6490
6719
  return serializeRemoteRegistryFollowerState();
@@ -3375,3 +3375,44 @@ body.mindcanvas-is-dense .css3d-resolution-wrapper.node-type-templatelauncher.se
3375
3375
  color: #93c5fd;
3376
3376
  }
3377
3377
  }
3378
+
3379
+ /* Lightweight CSS3D motion path.
3380
+ Reference graph tools are useful here because they keep node DOM cheap:
3381
+ while the camera is moving, retain the canonical CSS3D surface but remove
3382
+ decorative compositor-heavy effects from every CSS3D node, including
3383
+ selected and agent-style nodes. */
3384
+ body.is-zooming .css3d-resolution-wrapper,
3385
+ body.is-panning .css3d-resolution-wrapper,
3386
+ body.is-node-dragging .css3d-resolution-wrapper,
3387
+ body.is-resizing .css3d-resolution-wrapper,
3388
+ body.mindcanvas-is-moving .css3d-resolution-wrapper,
3389
+ body.mindcanvas-is-dense .css3d-resolution-wrapper {
3390
+ -webkit-transition: none !important;
3391
+ transition: none !important;
3392
+ -webkit-filter: none !important;
3393
+ filter: none !important;
3394
+ -webkit-box-shadow: none !important;
3395
+ box-shadow: none !important;
3396
+ }
3397
+
3398
+ body.is-zooming .css3d-resolution-wrapper .css3d-dynamic-node,
3399
+ body.is-panning .css3d-resolution-wrapper .css3d-dynamic-node,
3400
+ body.is-node-dragging .css3d-resolution-wrapper .css3d-dynamic-node,
3401
+ body.is-resizing .css3d-resolution-wrapper .css3d-dynamic-node,
3402
+ body.mindcanvas-is-moving .css3d-resolution-wrapper .css3d-dynamic-node,
3403
+ body.mindcanvas-is-dense .css3d-resolution-wrapper .css3d-dynamic-node,
3404
+ body.is-zooming .css3d-resolution-wrapper [id^="css3d-node-"],
3405
+ body.is-panning .css3d-resolution-wrapper [id^="css3d-node-"],
3406
+ body.is-node-dragging .css3d-resolution-wrapper [id^="css3d-node-"],
3407
+ body.is-resizing .css3d-resolution-wrapper [id^="css3d-node-"],
3408
+ body.mindcanvas-is-moving .css3d-resolution-wrapper [id^="css3d-node-"],
3409
+ body.mindcanvas-is-dense .css3d-resolution-wrapper [id^="css3d-node-"] {
3410
+ -webkit-transition: none !important;
3411
+ transition: none !important;
3412
+ -webkit-filter: none !important;
3413
+ filter: none !important;
3414
+ -webkit-backdrop-filter: none !important;
3415
+ backdrop-filter: none !important;
3416
+ -webkit-box-shadow: none !important;
3417
+ box-shadow: none !important;
3418
+ }
@@ -795,26 +795,26 @@
795
795
  max-height: none !important;
796
796
  }
797
797
 
798
- /* Preserve agent memo chrome during camera motion so middle-button panning
799
- does not visibly weaken their glow or console surfaces. */
800
- body.is-zooming .css3d-resolution-wrapper:not(.selection-style-agent),
801
- body.is-panning .css3d-resolution-wrapper:not(.selection-style-agent),
802
- body.mindcanvas-is-moving .css3d-resolution-wrapper:not(.selection-style-agent),
803
- body.mindcanvas-is-dense .css3d-resolution-wrapper:not(.selection-style-agent) {
798
+ /* Keep CSS3D nodes readable during camera motion, but drop expensive
799
+ decorative effects consistently across selected and agent-style nodes. */
800
+ body.is-zooming .css3d-resolution-wrapper,
801
+ body.is-panning .css3d-resolution-wrapper,
802
+ body.mindcanvas-is-moving .css3d-resolution-wrapper,
803
+ body.mindcanvas-is-dense .css3d-resolution-wrapper {
804
804
  transition: none !important;
805
805
  -webkit-transition: none !important;
806
806
  filter: none !important;
807
807
  -webkit-filter: none !important;
808
808
  }
809
809
 
810
- body.is-zooming .css3d-resolution-wrapper:not(.selection-style-agent) .css3d-dynamic-node,
811
- body.is-panning .css3d-resolution-wrapper:not(.selection-style-agent) .css3d-dynamic-node,
812
- body.mindcanvas-is-moving .css3d-resolution-wrapper:not(.selection-style-agent) .css3d-dynamic-node,
813
- body.mindcanvas-is-dense .css3d-resolution-wrapper:not(.selection-style-agent) .css3d-dynamic-node,
814
- body.is-zooming .css3d-resolution-wrapper:not(.selection-style-agent) [id^="css3d-node-"],
815
- body.is-panning .css3d-resolution-wrapper:not(.selection-style-agent) [id^="css3d-node-"],
816
- body.mindcanvas-is-moving .css3d-resolution-wrapper:not(.selection-style-agent) [id^="css3d-node-"],
817
- body.mindcanvas-is-dense .css3d-resolution-wrapper:not(.selection-style-agent) [id^="css3d-node-"] {
810
+ body.is-zooming .css3d-resolution-wrapper .css3d-dynamic-node,
811
+ body.is-panning .css3d-resolution-wrapper .css3d-dynamic-node,
812
+ body.mindcanvas-is-moving .css3d-resolution-wrapper .css3d-dynamic-node,
813
+ body.mindcanvas-is-dense .css3d-resolution-wrapper .css3d-dynamic-node,
814
+ body.is-zooming .css3d-resolution-wrapper [id^="css3d-node-"],
815
+ body.is-panning .css3d-resolution-wrapper [id^="css3d-node-"],
816
+ body.mindcanvas-is-moving .css3d-resolution-wrapper [id^="css3d-node-"],
817
+ body.mindcanvas-is-dense .css3d-resolution-wrapper [id^="css3d-node-"] {
818
818
  box-shadow: none !important;
819
819
  filter: none !important;
820
820
  -webkit-filter: none !important;
@@ -895,10 +895,10 @@
895
895
  body.is-panning .ai-loading-row.ai-loading-row-media,
896
896
  body.mindcanvas-is-moving .ai-loading-row.ai-loading-row-media,
897
897
  body.mindcanvas-is-dense .ai-loading-row.ai-loading-row-media,
898
- body.is-zooming .css3d-resolution-wrapper:not(.selection-style-agent) .map-node-memo__agent-console-panel,
899
- body.is-panning .css3d-resolution-wrapper:not(.selection-style-agent) .map-node-memo__agent-console-panel,
900
- body.mindcanvas-is-moving .css3d-resolution-wrapper:not(.selection-style-agent) .map-node-memo__agent-console-panel,
901
- body.mindcanvas-is-dense .css3d-resolution-wrapper:not(.selection-style-agent) .map-node-memo__agent-console-panel {
898
+ body.is-zooming .css3d-resolution-wrapper .map-node-memo__agent-console-panel,
899
+ body.is-panning .css3d-resolution-wrapper .map-node-memo__agent-console-panel,
900
+ body.mindcanvas-is-moving .css3d-resolution-wrapper .map-node-memo__agent-console-panel,
901
+ body.mindcanvas-is-dense .css3d-resolution-wrapper .map-node-memo__agent-console-panel {
902
902
  box-shadow: none !important;
903
903
  backdrop-filter: none !important;
904
904
  -webkit-backdrop-filter: none !important;
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "zdahFh/b",
2
+ "version": "RQBjk+TB",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -42,7 +42,7 @@
42
42
  "url": "_content/MindExecution.Shared/css/app.css"
43
43
  },
44
44
  {
45
- "hash": "sha256-29ALhCvhp5pZs7AXAjTpX+UCuf7jQSP6SQCn0p9Wb4Y=",
45
+ "hash": "sha256-vy6wZFJwYl0hLFMMhAKU4de+TPg7CJ91jMwnXl8GBqk=",
46
46
  "url": "_content/MindExecution.Shared/css/mind-map-overrides.css"
47
47
  },
48
48
  {
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-VJQ48zNB42JJETm59vTBtxdXez0dwIui6LnZUisNTSo=",
89
+ "hash": "sha256-f5vcy6AIq6e21hjCQlnjZnjQQ3s7wfbK4K6iW5IdBjU=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: zdahFh/b */
1
+ /* Manifest version: RQBjk+TB */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4