@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 +1 -1
- package/scripts/remote-registry-follower-smoke.mjs +75 -17
- package/server.js +249 -20
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +41 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +18 -18
- package/wwwroot/service-worker-assets.js +3 -3
- package/wwwroot/service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
170
|
-
assert.
|
|
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:
|
|
344
|
-
|
|
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
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5694
|
-
const session =
|
|
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
|
|
6315
|
-
const session =
|
|
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
|
|
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
|
|
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
|
-
/*
|
|
799
|
-
|
|
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
|
|
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
|
|
811
|
-
body.is-panning .css3d-resolution-wrapper
|
|
812
|
-
body.mindcanvas-is-moving .css3d-resolution-wrapper
|
|
813
|
-
body.mindcanvas-is-dense .css3d-resolution-wrapper
|
|
814
|
-
body.is-zooming .css3d-resolution-wrapper
|
|
815
|
-
body.is-panning .css3d-resolution-wrapper
|
|
816
|
-
body.mindcanvas-is-moving .css3d-resolution-wrapper
|
|
817
|
-
body.mindcanvas-is-dense .css3d-resolution-wrapper
|
|
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
|
|
899
|
-
body.is-panning .css3d-resolution-wrapper
|
|
900
|
-
body.mindcanvas-is-moving .css3d-resolution-wrapper
|
|
901
|
-
body.mindcanvas-is-dense .css3d-resolution-wrapper
|
|
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": "
|
|
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-
|
|
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-
|
|
89
|
+
"hash": "sha256-f5vcy6AIq6e21hjCQlnjZnjQQ3s7wfbK4K6iW5IdBjU=",
|
|
90
90
|
"url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
|
|
91
91
|
},
|
|
92
92
|
{
|