@fias/plugin-dev-harness 1.3.1 → 1.4.1

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.
@@ -44,7 +44,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.startHarnessServer = startHarnessServer;
46
46
  const express_1 = __importDefault(require("express"));
47
- const crypto = __importStar(require("crypto"));
48
47
  const path = __importStar(require("path"));
49
48
  const fs = __importStar(require("fs"));
50
49
  const chalk_1 = __importDefault(require("chalk"));
@@ -58,14 +57,11 @@ async function startHarnessServer(options) {
58
57
  // Mutable state — toggled at runtime via UI
59
58
  let currentMode = 'mock';
60
59
  let currentEnvironment = options.defaultEnvironment;
61
- let currentApiKey = (0, credentials_1.loadApiKeyForEnvironment)(currentEnvironment);
62
- let pendingAuthState = null;
60
+ let currentAuthToken = (0, credentials_1.loadAuthTokenForEnvironment)(currentEnvironment);
61
+ let currentSession = (0, credentials_1.loadSessionForEnvironment)(currentEnvironment);
63
62
  function getCurrentApiUrl() {
64
63
  return config_loader_1.ENVIRONMENT_URLS[currentEnvironment].apiUrl;
65
64
  }
66
- function getCurrentPlatformUrl() {
67
- return config_loader_1.ENVIRONMENT_URLS[currentEnvironment].platformUrl;
68
- }
69
65
  // Always create mock handler
70
66
  const mockHandler = new mock_handler_1.MockBridgeHandler({
71
67
  mockUser: options.mockUser,
@@ -73,9 +69,9 @@ async function startHarnessServer(options) {
73
69
  mockEntities: options.mockEntities,
74
70
  });
75
71
  // Create live handler (recreated when credentials or environment change)
76
- let liveHandler = currentApiKey
72
+ let liveHandler = currentAuthToken
77
73
  ? new live_handler_1.LiveBridgeHandler({
78
- apiKey: currentApiKey,
74
+ apiKey: currentAuthToken,
79
75
  apiUrl: getCurrentApiUrl(),
80
76
  permissions: options.permissions,
81
77
  mockUser: options.mockUser,
@@ -86,9 +82,9 @@ async function startHarnessServer(options) {
86
82
  return currentMode === 'live' && liveHandler ? liveHandler : mockHandler;
87
83
  }
88
84
  function rebuildLiveHandler() {
89
- if (currentApiKey) {
85
+ if (currentAuthToken) {
90
86
  liveHandler = new live_handler_1.LiveBridgeHandler({
91
- apiKey: currentApiKey,
87
+ apiKey: currentAuthToken,
92
88
  apiUrl: getCurrentApiUrl(),
93
89
  permissions: options.permissions,
94
90
  mockUser: options.mockUser,
@@ -99,12 +95,44 @@ async function startHarnessServer(options) {
99
95
  liveHandler = null;
100
96
  }
101
97
  }
98
+ /**
99
+ * Refreshes the session token if expired.
100
+ * Returns true if the token is valid (or was refreshed), false if re-login is needed.
101
+ */
102
+ async function ensureValidToken() {
103
+ if (!currentSession)
104
+ return Boolean(currentAuthToken); // API key or no credentials
105
+ if (currentSession.expiresAt > Date.now() + 60000)
106
+ return true; // Valid with 1min buffer
107
+ try {
108
+ const response = await fetch(`${getCurrentApiUrl()}/developer/auth/refresh`, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({ refreshToken: currentSession.refreshToken }),
112
+ });
113
+ if (!response.ok)
114
+ return false;
115
+ const data = (await response.json());
116
+ currentSession = {
117
+ idToken: data.idToken,
118
+ refreshToken: currentSession.refreshToken,
119
+ expiresAt: Date.now() + data.expiresIn * 1000,
120
+ };
121
+ currentAuthToken = data.idToken;
122
+ (0, credentials_1.saveSessionForEnvironment)(currentEnvironment, currentSession);
123
+ rebuildLiveHandler();
124
+ return true;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
102
130
  // Mode toggle endpoint — called by harness.js
103
131
  app.post('/api/mode', (req, res) => {
104
132
  const { mode } = req.body;
105
133
  if (mode === 'live') {
106
134
  if (!liveHandler) {
107
- res.status(400).json({ error: 'No API key configured. Use the login button first.' });
135
+ res.status(400).json({ error: 'Not authenticated. Sign in first.' });
108
136
  return;
109
137
  }
110
138
  currentMode = 'live';
@@ -115,49 +143,44 @@ async function startHarnessServer(options) {
115
143
  console.log(chalk_1.default.dim(` Mode switched to ${currentMode.toUpperCase()}`));
116
144
  res.json({ mode: currentMode });
117
145
  });
118
- // Login endpoint — saves API key and creates live handler
119
- app.post('/api/login', (req, res) => {
120
- const { apiKey } = req.body;
121
- if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) {
122
- res.status(400).json({ error: 'API key is required' });
146
+ // Auth login endpoint — authenticates with the platform and stores session
147
+ app.post('/api/auth/login', async (req, res) => {
148
+ const { email, password } = req.body;
149
+ if (!email || !password) {
150
+ res.status(400).json({ error: 'Email and password are required' });
123
151
  return;
124
152
  }
125
- currentApiKey = apiKey.trim();
126
- (0, credentials_1.saveApiKeyForEnvironment)(currentEnvironment, currentApiKey);
127
- rebuildLiveHandler();
128
- console.log(chalk_1.default.green(` API key saved for ${currentEnvironment} environment`));
129
- res.json({ success: true });
130
- });
131
- // Auth start endpoint — generates state token and returns auth URL for popup
132
- app.post('/api/auth/start', (_req, res) => {
133
- pendingAuthState = crypto.randomBytes(32).toString('hex');
134
- const authUrl = `${getCurrentPlatformUrl()}/developer/cli-auth?port=${options.port}&state=${pendingAuthState}`;
135
- res.json({ authUrl });
136
- });
137
- // Auth callback receives redirect from CliAuthPage after browser login
138
- app.get('/callback', (req, res) => {
139
- const key = req.query.key;
140
- const returnedState = req.query.state;
141
- res.setHeader('Content-Type', 'text/html');
142
- if (!pendingAuthState || returnedState !== pendingAuthState) {
143
- res
144
- .status(400)
145
- .send(renderCallbackHtml('Authentication Failed', 'State mismatch. Please try again.', true));
146
- return;
153
+ try {
154
+ const response = await fetch(`${getCurrentApiUrl()}/developer/auth`, {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify({ email, password }),
158
+ });
159
+ const data = (await response.json());
160
+ if (!response.ok) {
161
+ res.status(response.status).json({ error: data.error?.message || 'Authentication failed' });
162
+ return;
163
+ }
164
+ if (!data.idToken || !data.refreshToken) {
165
+ res.status(500).json({ error: 'Invalid response from auth service' });
166
+ return;
167
+ }
168
+ currentSession = {
169
+ idToken: data.idToken,
170
+ refreshToken: data.refreshToken,
171
+ expiresAt: Date.now() + (data.expiresIn || 3600) * 1000,
172
+ };
173
+ currentAuthToken = data.idToken;
174
+ (0, credentials_1.saveSessionForEnvironment)(currentEnvironment, currentSession);
175
+ rebuildLiveHandler();
176
+ console.log(chalk_1.default.green(` Authenticated for ${currentEnvironment} environment`));
177
+ res.json({ success: true, user: data.user });
147
178
  }
148
- if (!key || !key.startsWith('fias_sk_')) {
149
- res
150
- .status(400)
151
- .send(renderCallbackHtml('Authentication Failed', 'Invalid API key received.', true));
152
- return;
179
+ catch (err) {
180
+ res.status(500).json({
181
+ error: err instanceof Error ? err.message : 'Failed to connect to auth service',
182
+ });
153
183
  }
154
- // Save key for current environment
155
- currentApiKey = key;
156
- (0, credentials_1.saveApiKeyForEnvironment)(currentEnvironment, currentApiKey);
157
- rebuildLiveHandler();
158
- pendingAuthState = null;
159
- console.log(chalk_1.default.green(` Authenticated for ${currentEnvironment} environment`));
160
- res.send(renderCallbackHtml('Authentication Successful', 'You can close this window. The harness will switch to live mode automatically.', false));
161
184
  });
162
185
  // Environment switch endpoint
163
186
  app.post('/api/environment', (req, res) => {
@@ -167,9 +190,10 @@ async function startHarnessServer(options) {
167
190
  return;
168
191
  }
169
192
  currentEnvironment = environment;
170
- currentApiKey = (0, credentials_1.loadApiKeyForEnvironment)(currentEnvironment);
193
+ currentAuthToken = (0, credentials_1.loadAuthTokenForEnvironment)(currentEnvironment);
194
+ currentSession = (0, credentials_1.loadSessionForEnvironment)(currentEnvironment);
171
195
  rebuildLiveHandler();
172
- // Fall back to mock if no key for this environment
196
+ // Fall back to mock if no credentials for this environment
173
197
  if (currentMode === 'live' && !liveHandler) {
174
198
  currentMode = 'mock';
175
199
  }
@@ -177,11 +201,20 @@ async function startHarnessServer(options) {
177
201
  res.json({
178
202
  environment: currentEnvironment,
179
203
  mode: currentMode,
180
- hasCredentials: Boolean(currentApiKey),
204
+ hasCredentials: Boolean(currentAuthToken),
181
205
  });
182
206
  });
183
207
  // Bridge API endpoint — called by harness.js
184
208
  app.post('/api/bridge', async (req, res) => {
209
+ // Refresh token if needed before proxying
210
+ if (currentMode === 'live') {
211
+ const valid = await ensureValidToken();
212
+ if (!valid) {
213
+ currentMode = 'mock';
214
+ res.status(401).json({ error: 'Session expired. Please sign in again.' });
215
+ return;
216
+ }
217
+ }
185
218
  try {
186
219
  const result = await getActiveHandler().handle(req.body);
187
220
  res.json(result);
@@ -194,13 +227,13 @@ async function startHarnessServer(options) {
194
227
  });
195
228
  // Credit balance endpoint
196
229
  app.get('/api/credits', async (_req, res) => {
197
- if (currentMode !== 'live' || !currentApiKey) {
230
+ if (currentMode !== 'live' || !currentAuthToken) {
198
231
  res.json({ balance: null });
199
232
  return;
200
233
  }
201
234
  try {
202
235
  const response = await fetch(`${getCurrentApiUrl()}/developer/credits`, {
203
- headers: { Authorization: `Bearer ${currentApiKey}` },
236
+ headers: { Authorization: `Bearer ${currentAuthToken}` },
204
237
  });
205
238
  if (!response.ok) {
206
239
  res.status(response.status).json({ error: 'Failed to fetch credits' });
@@ -214,7 +247,7 @@ async function startHarnessServer(options) {
214
247
  .json({ error: err instanceof Error ? err.message : 'Failed to fetch credits' });
215
248
  }
216
249
  });
217
- // Plugin reachability check — called by harness.js before loading iframe
250
+ // Plugin reachability check
218
251
  app.get('/api/check-plugin', async (_req, res) => {
219
252
  try {
220
253
  const controller = new AbortController();
@@ -227,21 +260,19 @@ async function startHarnessServer(options) {
227
260
  res.json({ reachable: false });
228
261
  }
229
262
  });
230
- // Harness config endpoint — provides config to the frontend
263
+ // Harness config endpoint
231
264
  app.get('/api/config', (_req, res) => {
232
265
  res.json({
233
266
  pluginUrl: options.pluginUrl,
234
267
  mode: currentMode,
235
- hasCredentials: Boolean(currentApiKey),
268
+ hasCredentials: Boolean(currentAuthToken),
236
269
  permissions: options.permissions,
237
270
  mockTheme: options.mockTheme,
238
271
  environment: currentEnvironment,
239
- platformUrl: getCurrentPlatformUrl(),
240
272
  });
241
273
  });
242
274
  // Serve static harness files
243
275
  const staticDir = path.join(__dirname, 'static');
244
- // In development (ts-node) vs built (dist), static may be in different locations
245
276
  const possibleStaticDirs = [
246
277
  staticDir,
247
278
  path.join(__dirname, '..', 'server', 'static'),
@@ -255,7 +286,6 @@ async function startHarnessServer(options) {
255
286
  }
256
287
  }
257
288
  app.use('/static', express_1.default.static(resolvedStaticDir));
258
- // Serve harness HTML at root
259
289
  app.get('/', (_req, res) => {
260
290
  const htmlPath = path.join(resolvedStaticDir, 'harness.html');
261
291
  if (fs.existsSync(htmlPath)) {
@@ -273,7 +303,6 @@ async function startHarnessServer(options) {
273
303
  console.log(chalk_1.default.dim(` Mode: MOCK (toggle to LIVE in the toolbar)`));
274
304
  console.log(chalk_1.default.dim(` Environment: ${currentEnvironment.toUpperCase()}`));
275
305
  console.log('');
276
- // Check if plugin dev server is reachable
277
306
  checkPluginReachable(options.pluginUrl).then((reachable) => {
278
307
  if (reachable) {
279
308
  console.log(chalk_1.default.green(` ✓ Plugin server is reachable at ${options.pluginUrl}\n`));
@@ -288,32 +317,6 @@ async function startHarnessServer(options) {
288
317
  });
289
318
  });
290
319
  }
291
- /**
292
- * Renders the HTML page shown in the popup after auth callback.
293
- * Sends a postMessage to the opener window and auto-closes.
294
- */
295
- function renderCallbackHtml(title, message, isError) {
296
- const color = isError ? '#ef4444' : '#22c55e';
297
- const postMessageScript = isError
298
- ? `if(window.opener){window.opener.postMessage({type:'auth_callback',success:false,error:${JSON.stringify(title)}},'*');}`
299
- : `if(window.opener){window.opener.postMessage({type:'auth_callback',success:true},'*');setTimeout(function(){window.close()},1500);}`;
300
- return `<!DOCTYPE html>
301
- <html>
302
- <head><title>${title}</title></head>
303
- <body style="font-family: -apple-system, system-ui, sans-serif; display: flex; justify-content: center; padding-top: 80px; background: #111; color: #e5e7eb;">
304
- <div style="text-align: center; max-width: 400px;">
305
- <h1 style="color: ${color}; font-size: 1.5rem;">${title}</h1>
306
- <p style="margin-top: 12px; color: #9ca3af;">${message}</p>
307
- </div>
308
- <script>${postMessageScript}</script>
309
- </body>
310
- </html>`;
311
- }
312
- /**
313
- * Fallback HTML when static files haven't been copied to dist.
314
- * This enables the harness to work during development.
315
- * Mirrors the structure of harness.html but with inline styles/scripts.
316
- */
317
320
  function generateFallbackHtml(options) {
318
321
  return `<!DOCTYPE html>
319
322
  <html lang="en">
@@ -331,9 +334,7 @@ function generateFallbackHtml(options) {
331
334
  <option value="staging"${options.defaultEnvironment === 'staging' ? ' selected' : ''}>STAGING</option>
332
335
  <option value="production"${options.defaultEnvironment === 'production' ? ' selected' : ''}>PRODUCTION</option>
333
336
  </select>
334
- <button id="mode-badge" class="mode-badge mode-mock" title="Click to switch mode">
335
- MOCK \u21C6
336
- </button>
337
+ <button id="mode-badge" class="mode-badge mode-mock" title="Click to switch mode">MOCK \\u21C6</button>
337
338
  </div>
338
339
  <div class="toolbar-right">
339
340
  <span id="credit-balance" class="credit-balance" style="display: none"></span>
@@ -344,38 +345,24 @@ function generateFallbackHtml(options) {
344
345
  </div>
345
346
  <div id="login-modal" class="modal-overlay" style="display: none">
346
347
  <div class="modal">
347
- <h3>Connect to FIAS</h3>
348
- <p>Sign in to your FIAS account to enable live mode with real AI responses.</p>
349
- <div id="login-popup-section">
350
- <button id="login-popup-btn" class="btn-primary btn-full">Sign in with FIAS</button>
351
- <p id="login-popup-hint" class="modal-hint" style="display:none">Popup blocked? <a id="login-popup-link" href="#" target="_blank" style="color:#a78bfa">Open login page manually</a></p>
352
- <div class="modal-divider"><span>or</span></div>
353
- <button id="login-manual-toggle" class="btn-link">Enter API key manually</button>
354
- </div>
355
- <div id="login-manual-section" style="display:none">
356
- <input id="login-input" type="password" class="modal-input" placeholder="fias_sk_..." autocomplete="off" />
357
- <div class="modal-actions">
358
- <button id="login-cancel" class="btn-secondary">Cancel</button>
359
- <button id="login-submit" class="btn-primary">Save &amp; Connect</button>
360
- </div>
361
- </div>
348
+ <h3>Sign in to FIAS</h3>
349
+ <p>Signing in to <strong id="login-target"></strong></p>
350
+ <input id="login-email" type="email" class="modal-input" placeholder="Email" autocomplete="email" />
351
+ <input id="login-password" type="password" class="modal-input" placeholder="Password" autocomplete="current-password" />
362
352
  <div id="login-error" class="modal-error" style="display: none"></div>
353
+ <div class="modal-actions">
354
+ <button id="login-cancel" class="btn-secondary">Cancel</button>
355
+ <button id="login-submit" class="btn-primary">Sign in</button>
356
+ </div>
363
357
  </div>
364
358
  </div>
365
359
  <div id="plugin-status" class="plugin-status">
366
360
  <div class="status-spinner"></div>
367
361
  <p>Connecting to plugin server...</p>
368
362
  </div>
369
- <iframe
370
- id="plugin-iframe"
371
- class="hidden"
372
- sandbox="allow-scripts allow-forms allow-same-origin"
373
- ></iframe>
363
+ <iframe id="plugin-iframe" class="hidden" sandbox="allow-scripts allow-forms allow-same-origin"></iframe>
374
364
  <div id="console-panel" class="console-panel">
375
- <div class="console-header" id="console-toggle">
376
- <span>Dev Console</span>
377
- <span id="console-count">0 messages</span>
378
- </div>
365
+ <div class="console-header" id="console-toggle"><span>Dev Console</span><span id="console-count">0 messages</span></div>
379
366
  <div class="console-body" id="console-body"></div>
380
367
  </div>
381
368
  <script>${EMBEDDED_JS}</script>
@@ -394,8 +381,7 @@ async function checkPluginReachable(url) {
394
381
  return false;
395
382
  }
396
383
  }
397
- const EMBEDDED_CSS = `
398
- * { margin: 0; padding: 0; box-sizing: border-box; }
384
+ const EMBEDDED_CSS = `* { margin: 0; padding: 0; box-sizing: border-box; }
399
385
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0f; color: #e4e4e7; display: flex; flex-direction: column; height: 100vh; }
400
386
  .toolbar { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: #18181b; border-bottom: 1px solid #3f3f46; flex-shrink: 0; }
401
387
  .toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 12px; }
@@ -403,9 +389,8 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
403
389
  .env-selector { background: #27272a; color: #a1a1aa; border: 1px solid #3f3f46; border-radius: 4px; padding: 3px 8px; font-size: 11px; font-weight: 600; cursor: pointer; outline: none; text-transform: uppercase; }
404
390
  .env-selector:hover { border-color: #52525b; }
405
391
  .env-selector:focus { border-color: #a78bfa; }
406
- .mode-badge { padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; cursor: pointer; transition: background 0.15s, border-color 0.15s; user-select: none; }
392
+ .mode-badge { padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; cursor: pointer; transition: background 0.15s; user-select: none; }
407
393
  .mode-badge:hover { filter: brightness(1.3); }
408
- .mode-badge:active { filter: brightness(0.9); }
409
394
  .mode-mock { background: #166534; color: #86efac; border: 1px solid #22c55e; }
410
395
  .mode-live { background: #854d0e; color: #fde047; border: 1px solid #eab308; }
411
396
  .theme-badge { padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
@@ -417,22 +402,16 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
417
402
  .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; }
418
403
  .modal { background: #27272a; border: 1px solid #3f3f46; border-radius: 8px; padding: 24px; width: 420px; max-width: 90vw; }
419
404
  .modal h3 { font-size: 16px; color: #e4e4e7; margin-bottom: 8px; }
420
- .modal p { font-size: 13px; color: #a1a1aa; line-height: 1.5; margin-bottom: 4px; }
421
- .modal-hint { font-size: 12px !important; color: #6b7280 !important; margin-bottom: 16px !important; }
422
- .modal-input { width: 100%; padding: 8px 12px; background: #18181b; border: 1px solid #3f3f46; border-radius: 4px; color: #e4e4e7; font-family: monospace; font-size: 13px; margin-bottom: 12px; outline: none; }
405
+ .modal p { font-size: 13px; color: #a1a1aa; line-height: 1.5; margin-bottom: 16px; }
406
+ .modal-input { width: 100%; padding: 8px 12px; background: #18181b; border: 1px solid #3f3f46; border-radius: 4px; color: #e4e4e7; font-size: 13px; margin-bottom: 12px; outline: none; }
423
407
  .modal-input:focus { border-color: #a78bfa; }
424
408
  .modal-error { font-size: 12px; color: #fca5a5; margin-bottom: 12px; }
425
409
  .modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
426
- .btn-primary { background: #ffffff; color: #0a0a0a; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; }
410
+ .btn-primary { background: #fff; color: #0a0a0a; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; }
427
411
  .btn-primary:hover { background: #e5e5e5; }
428
412
  .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
429
- .btn-full { width: 100%; padding: 10px 16px; margin-bottom: 12px; }
430
413
  .btn-secondary { background: transparent; color: #a1a1aa; border: 1px solid #3f3f46; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
431
414
  .btn-secondary:hover { background: #3f3f46; }
432
- .btn-link { background: none; border: none; color: #a78bfa; cursor: pointer; font-size: 12px; padding: 0; }
433
- .btn-link:hover { text-decoration: underline; }
434
- .modal-divider { display: flex; align-items: center; gap: 12px; margin: 12px 0; color: #6b7280; font-size: 12px; }
435
- .modal-divider::before, .modal-divider::after { content: ''; flex: 1; height: 1px; background: #3f3f46; }
436
415
  .plugin-status { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; color: #a1a1aa; font-size: 14px; }
437
416
  .plugin-status.hidden { display: none; }
438
417
  .plugin-status.error { color: #fca5a5; }
@@ -443,253 +422,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
443
422
  #plugin-iframe { flex: 1; border: none; width: 100%; }
444
423
  #plugin-iframe.hidden { display: none; }
445
424
  .console-panel { flex-shrink: 0; border-top: 1px solid #3f3f46; background: #18181b; max-height: 250px; display: flex; flex-direction: column; }
446
- .console-header { display: flex; justify-content: space-between; padding: 6px 16px; cursor: pointer; font-size: 12px; color: #a1a1aa; border-bottom: 1px solid #27272a; }
425
+ .console-header { display: flex; justify-content: space-between; padding: 6px 16px; cursor: pointer; font-size: 12px; color: #a1a1aa; }
447
426
  .console-body { overflow-y: auto; padding: 8px 16px; font-family: monospace; font-size: 11px; flex: 1; display: none; }
448
427
  .console-body.open { display: block; }
449
428
  .log-entry { padding: 2px 0; border-bottom: 1px solid #1e1e22; }
450
429
  .log-info { color: #93c5fd; }
451
430
  .log-warn { color: #fde047; }
452
431
  .log-error { color: #fca5a5; }
453
- .log-time { color: #6b7280; margin-right: 8px; }
454
- .log-cost { color: #fbbf24; margin-left: 8px; }
455
- `;
456
- // The embedded JS is a minified version of harness.js for the fallback page.
457
- const EMBEDDED_JS = `
458
- (function() {
459
- var iframe = document.getElementById('plugin-iframe');
460
- var consoleBody = document.getElementById('console-body');
461
- var consoleCount = document.getElementById('console-count');
462
- var consoleToggle = document.getElementById('console-toggle');
463
- var creditBalance = document.getElementById('credit-balance');
464
- var themeToggle = document.getElementById('theme-toggle');
465
- var reloadBtn = document.getElementById('reload-btn');
466
- var modeBadge = document.getElementById('mode-badge');
467
- var themeBadge = document.getElementById('theme-badge');
468
- var pluginStatus = document.getElementById('plugin-status');
469
- var loginModal = document.getElementById('login-modal');
470
- var loginInput = document.getElementById('login-input');
471
- var loginError = document.getElementById('login-error');
472
- var loginSubmit = document.getElementById('login-submit');
473
- var loginCancel = document.getElementById('login-cancel');
474
- var envSelector = document.getElementById('env-selector');
475
- var loginPopupBtn = document.getElementById('login-popup-btn');
476
- var loginPopupHint = document.getElementById('login-popup-hint');
477
- var loginPopupLink = document.getElementById('login-popup-link');
478
- var loginManualToggle = document.getElementById('login-manual-toggle');
479
- var loginManualSection = document.getElementById('login-manual-section');
480
- var loginPopupSection = document.getElementById('login-popup-section');
481
-
482
- var messageCount = 0;
483
- var currentTheme = 'dark';
484
- var currentMode = 'mock';
485
- var currentEnvironment = 'staging';
486
- var hasCredentials = false;
487
- var cachedConfig = null;
488
- var authPollInterval = null;
489
-
490
- var PERMISSION_MAP = {
491
- get_user: 'user:profile:read', get_theme: 'theme:read', entity_invoke: 'entities:invoke',
492
- storage_read: 'storage:sandbox', storage_write: 'storage:sandbox',
493
- storage_list: 'storage:sandbox', storage_delete: 'storage:sandbox',
494
- };
495
- var RATE_LIMITS = {
496
- entity_invoke: { maxPerMinute: 60 }, storage_write: { maxPerMinute: 120 },
497
- storage_read: { maxPerMinute: 300 }, storage_list: { maxPerMinute: 60 },
498
- storage_delete: { maxPerMinute: 60 },
499
- };
500
- var rateBuckets = {};
501
-
502
- var SYSTEM_FONTS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
503
- var MONO_FONTS = '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace';
504
- var SPACING = { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' };
505
- var FONTS = { body: SYSTEM_FONTS, heading: SYSTEM_FONTS, mono: MONO_FONTS };
506
-
507
- fetch('/api/config').then(function(r) { return r.json(); }).then(function(config) {
508
- cachedConfig = config;
509
- currentTheme = config.mockTheme || 'dark';
510
- currentMode = config.mode || 'mock';
511
- currentEnvironment = config.environment || 'staging';
512
- hasCredentials = config.hasCredentials || false;
513
- envSelector.value = currentEnvironment;
514
- updateThemeBadge();
515
- updateModeBadge();
516
- if (currentMode === 'live') { creditBalance.style.display = 'inline'; fetchCredits(); }
517
- checkPluginReachable(function(reachable) {
518
- if (reachable) { pluginStatus.classList.add('hidden'); iframe.classList.remove('hidden'); iframe.src = config.pluginUrl; }
519
- else { pluginStatus.classList.add('error'); pluginStatus.innerHTML = '<p><strong>Plugin server not reachable</strong></p><p>Make sure your plugin dev server is running.</p>'; iframe.classList.add('hidden'); }
520
- });
521
- });
522
-
523
- consoleToggle.addEventListener('click', function() { consoleBody.classList.toggle('open'); });
524
- reloadBtn.addEventListener('click', function() { if (cachedConfig) { iframe.src = cachedConfig.pluginUrl; } });
525
-
526
- themeToggle.addEventListener('click', function() {
527
- currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
528
- updateThemeBadge();
529
- sendToPlugin({ type: 'theme_update', messageId: 'theme_' + Date.now(), payload: getTheme() });
530
- });
531
-
532
- modeBadge.addEventListener('click', function() {
533
- if (currentMode === 'mock') {
534
- if (!hasCredentials) { showLoginModal(); return; }
535
- switchMode('live');
536
- } else { switchMode('mock'); }
537
- });
538
-
539
- envSelector.addEventListener('change', function() {
540
- fetch('/api/environment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ environment: envSelector.value }) })
541
- .then(function(r) { return r.json(); })
542
- .then(function(data) {
543
- currentEnvironment = data.environment;
544
- currentMode = data.mode;
545
- hasCredentials = data.hasCredentials;
546
- updateModeBadge();
547
- if (currentMode === 'live') { creditBalance.style.display = 'inline'; fetchCredits(); }
548
- else { creditBalance.style.display = 'none'; creditBalance.textContent = ''; }
549
- logMessage('info', 'Environment: ' + currentEnvironment.toUpperCase() + (hasCredentials ? '' : ' (not authenticated)'));
550
- }).catch(function(err) { logMessage('error', err.message); });
551
- });
552
-
553
- function switchMode(newMode) {
554
- fetch('/api/mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: newMode }) })
555
- .then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.error); }); return r.json(); })
556
- .then(function(data) {
557
- currentMode = data.mode; updateModeBadge();
558
- if (currentMode === 'live') { creditBalance.style.display = 'inline'; fetchCredits(); }
559
- else { creditBalance.style.display = 'none'; creditBalance.textContent = ''; }
560
- }).catch(function(err) { logMessage('error', err.message); });
561
- }
562
-
563
- function showLoginModal() {
564
- loginModal.style.display = 'flex';
565
- loginPopupSection.style.display = 'block';
566
- loginManualSection.style.display = 'none';
567
- loginPopupHint.style.display = 'none';
568
- loginError.style.display = 'none';
569
- loginPopupBtn.disabled = false;
570
- loginPopupBtn.textContent = 'Sign in with FIAS';
571
- }
572
- function hideLoginModal() { loginModal.style.display = 'none'; stopAuthPolling(); }
573
-
574
- loginCancel && loginCancel.addEventListener('click', hideLoginModal);
575
- loginModal.addEventListener('click', function(e) { if (e.target === loginModal) hideLoginModal(); });
576
-
577
- loginManualToggle && loginManualToggle.addEventListener('click', function() {
578
- loginPopupSection.style.display = 'none';
579
- loginManualSection.style.display = 'block';
580
- loginInput.focus();
581
- });
582
-
583
- loginInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') submitLogin(); if (e.key === 'Escape') hideLoginModal(); });
584
- loginSubmit.addEventListener('click', submitLogin);
585
-
586
- loginPopupBtn && loginPopupBtn.addEventListener('click', function() {
587
- loginPopupBtn.disabled = true;
588
- loginPopupBtn.textContent = 'Opening...';
589
- fetch('/api/auth/start', { method: 'POST' })
590
- .then(function(r) { return r.json(); })
591
- .then(function(data) {
592
- var popup = window.open(data.authUrl, 'fias-auth', 'width=500,height=650');
593
- if (!popup || popup.closed) {
594
- loginPopupHint.style.display = 'block';
595
- loginPopupLink.href = data.authUrl;
596
- loginPopupBtn.disabled = false;
597
- loginPopupBtn.textContent = 'Sign in with FIAS';
598
- } else {
599
- loginPopupBtn.textContent = 'Waiting for sign-in...';
600
- }
601
- startAuthPolling();
602
- }).catch(function(err) {
603
- loginError.textContent = err.message;
604
- loginError.style.display = 'block';
605
- loginPopupBtn.disabled = false;
606
- loginPopupBtn.textContent = 'Sign in with FIAS';
607
- });
608
- });
609
-
610
- window.addEventListener('message', function(event) {
611
- if (event.data && event.data.type === 'auth_callback') {
612
- stopAuthPolling();
613
- if (event.data.success) {
614
- hasCredentials = true;
615
- hideLoginModal();
616
- logMessage('info', 'Authenticated for ' + currentEnvironment.toUpperCase());
617
- switchMode('live');
618
- } else {
619
- loginError.textContent = event.data.error || 'Authentication failed';
620
- loginError.style.display = 'block';
621
- }
622
- loginPopupBtn.disabled = false;
623
- loginPopupBtn.textContent = 'Sign in with FIAS';
624
- return;
625
- }
626
- if (event.source !== iframe.contentWindow) return;
627
- var data = event.data;
628
- if (!data || typeof data !== 'object' || !data.type || !data.messageId) return;
629
- logMessage('recv', data.type, data.payload);
630
- if (data.type === 'ready') return;
631
- if (data.type === 'resize') { var h = data.payload && data.payload.height; if (typeof h === 'number' && h > 0) { iframe.style.height = h + 'px'; iframe.style.flex = 'none'; } return; }
632
- if (data.type === 'toast') { var msg = data.payload && data.payload.message; if (typeof msg === 'string') logMessage('toast', msg); return; }
633
- if (data.type === 'navigate') { var p = data.payload && data.payload.path; if (typeof p === 'string') logMessage('nav', p); return; }
634
- handleRequest(data);
635
- });
636
-
637
- function handleRequest(data) {
638
- try {
639
- var requiredPerm = PERMISSION_MAP[data.type];
640
- if (requiredPerm && cachedConfig && cachedConfig.permissions.indexOf(requiredPerm) === -1) throw new Error('Permission denied: ' + requiredPerm);
641
- checkRateLimit(data.type);
642
- } catch (err) { logMessage('error', err.message); sendToPlugin({ type: 'response', messageId: data.messageId, payload: null, error: err.message }); return; }
643
- fetch('/api/bridge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: data.type, payload: data.payload }) })
644
- .then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || 'Bridge call failed'); }); return r.json(); })
645
- .then(function(result) {
646
- logMessage('send', 'response', result);
647
- if (data.type === 'entity_invoke' && result.metadata && result.metadata.cost > 0) { logMessage('cost', 'Credits: ' + result.metadata.cost.toFixed(4)); fetchCredits(); }
648
- sendToPlugin({ type: 'response', messageId: data.messageId, payload: result });
649
- })
650
- .catch(function(err) { logMessage('error', err.message); sendToPlugin({ type: 'response', messageId: data.messageId, payload: null, error: err.message }); });
651
- }
652
-
653
- iframe.addEventListener('load', function() {
654
- if (!cachedConfig) return;
655
- sendToPlugin({ type: 'init', messageId: 'init_0', payload: { archId: 'dev_harness', permissions: cachedConfig.permissions, theme: getTheme(), currentPath: '/' } });
656
- logMessage('send', 'init');
657
- });
658
-
659
- function startAuthPolling() { if (authPollInterval) return; authPollInterval = setInterval(function() { fetch('/api/config').then(function(r){return r.json();}).then(function(c){ if(c.hasCredentials&&!hasCredentials){hasCredentials=true;stopAuthPolling();hideLoginModal();logMessage('info','Authenticated for '+currentEnvironment.toUpperCase());switchMode('live');} }).catch(function(){}); }, 2000); }
660
- function stopAuthPolling() { if (authPollInterval) { clearInterval(authPollInterval); authPollInterval = null; } }
661
- function updateModeBadge() {
662
- modeBadge.textContent = currentMode.toUpperCase() + ' \\u21C6';
663
- modeBadge.className = 'mode-badge ' + (currentMode === 'live' ? 'mode-live' : 'mode-mock');
664
- modeBadge.title = 'Click to switch to ' + (currentMode === 'live' ? 'Mock' : 'Live') + ' mode';
665
- }
666
- function updateThemeBadge() { themeBadge.textContent = currentTheme.toUpperCase(); themeBadge.className = 'theme-badge theme-' + currentTheme; document.body.style.background = currentTheme === 'light' ? '#ffffff' : '#0a0a0a'; }
667
- function sendToPlugin(message) { iframe.contentWindow && iframe.contentWindow.postMessage(message, '*'); }
668
- function checkRateLimit(type) { var limit = RATE_LIMITS[type]; if (!limit) return; var now = Date.now(); var bucket = rateBuckets[type]; if (!bucket || now - bucket.windowStart > 60000) { rateBuckets[type] = { count: 1, windowStart: now }; return; } if (bucket.count >= limit.maxPerMinute) throw new Error('Rate limit exceeded for ' + type); bucket.count++; }
669
- function getTheme() {
670
- if (currentTheme === 'light') return { mode: 'light', colors: { primary: '#171717', secondary: '#e5e5e5', background: '#ffffff', surface: '#fafafa', text: '#0a0a0a', textSecondary: '#737373', border: '#e5e5e5', error: '#dc2626', warning: '#d97706', success: '#16a34a' }, spacing: SPACING, fonts: FONTS };
671
- return { mode: 'dark', colors: { primary: '#ffffff', secondary: '#1f1f1f', background: '#0a0a0a', surface: '#171717', text: '#ffffff', textSecondary: '#a6a6a6', border: '#2e2e2e', error: '#ef4444', warning: '#f59e0b', success: '#22c55e' }, spacing: SPACING, fonts: FONTS };
672
- }
673
- function checkPluginReachable(cb) { fetch('/api/check-plugin').then(function(r) { return r.json(); }).then(function(d) { cb(d.reachable); }).catch(function() { cb(true); }); }
674
- function fetchCredits() { fetch('/api/credits').then(function(r) { return r.json(); }).then(function(d) { if (d.balance != null && isFinite(d.balance)) creditBalance.textContent = 'Credits: ' + d.balance.toFixed(2); }).catch(function() {}); }
675
- function logMessage(dir, type, payload) {
676
- messageCount++; consoleCount.textContent = messageCount + ' messages';
677
- var entry = document.createElement('div'); entry.className = 'log-entry';
678
- var cls = 'log-info'; if (dir === 'error') cls = 'log-error'; if (dir === 'warn' || dir === 'cost' || dir === 'toast') cls = 'log-warn';
679
- var text = '<span class="log-time">' + new Date().toLocaleTimeString() + '</span><span class="' + cls + '">[' + dir.toUpperCase() + '] ' + type + '</span>';
680
- if (payload && typeof payload === 'object') text += ' <span style="color:#6b7280">' + JSON.stringify(payload).substring(0, 120) + '</span>';
681
- entry.innerHTML = text; consoleBody.appendChild(entry); consoleBody.scrollTop = consoleBody.scrollHeight;
682
- }
683
- function submitLogin() {
684
- var apiKey = loginInput.value.trim();
685
- if (!apiKey) { loginError.textContent = 'Please enter an API key.'; loginError.style.display = 'block'; return; }
686
- loginSubmit.disabled = true; loginSubmit.textContent = 'Connecting...';
687
- fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: apiKey }) })
688
- .then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.error); }); return r.json(); })
689
- .then(function() { hasCredentials = true; hideLoginModal(); switchMode('live'); })
690
- .catch(function(err) { loginError.textContent = err.message; loginError.style.display = 'block'; })
691
- .finally(function() { loginSubmit.disabled = false; loginSubmit.textContent = 'Save & Connect'; });
692
- }
693
- })();
694
- `;
432
+ .log-time { color: #6b7280; margin-right: 8px; }`;
433
+ const EMBEDDED_JS = `(function(){var iframe=document.getElementById('plugin-iframe'),consoleBody=document.getElementById('console-body'),consoleCount=document.getElementById('console-count'),consoleToggle=document.getElementById('console-toggle'),creditBalance=document.getElementById('credit-balance'),themeToggle=document.getElementById('theme-toggle'),reloadBtn=document.getElementById('reload-btn'),modeBadge=document.getElementById('mode-badge'),themeBadge=document.getElementById('theme-badge'),pluginStatus=document.getElementById('plugin-status'),loginModal=document.getElementById('login-modal'),loginEmail=document.getElementById('login-email'),loginPassword=document.getElementById('login-password'),loginError=document.getElementById('login-error'),loginSubmit=document.getElementById('login-submit'),loginCancel=document.getElementById('login-cancel'),envSelector=document.getElementById('env-selector'),loginTarget=document.getElementById('login-target');var messageCount=0,currentTheme='dark',currentMode='mock',currentEnvironment='staging',hasCredentials=false,cachedConfig=null;var PM={get_user:'user:profile:read',get_theme:'theme:read',entity_invoke:'entities:invoke',storage_read:'storage:sandbox',storage_write:'storage:sandbox',storage_list:'storage:sandbox',storage_delete:'storage:sandbox'};var RL={entity_invoke:{m:60},storage_write:{m:120},storage_read:{m:300},storage_list:{m:60},storage_delete:{m:60}};var rb={};var SF='-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',MF='"SFMono-Regular",Consolas,monospace',SP={xs:'4px',sm:'8px',md:'16px',lg:'24px',xl:'32px'},FN={body:SF,heading:SF,mono:MF};fetch('/api/config').then(function(r){return r.json()}).then(function(c){cachedConfig=c;currentTheme=c.mockTheme||'dark';currentMode=c.mode||'mock';currentEnvironment=c.environment||'staging';hasCredentials=c.hasCredentials||false;envSelector.value=currentEnvironment;uMB();uTB();if(currentMode==='live'){creditBalance.style.display='inline';fC()}cPR(function(ok){if(ok){pluginStatus.classList.add('hidden');iframe.classList.remove('hidden');iframe.src=c.pluginUrl}else{pluginStatus.classList.add('error');pluginStatus.innerHTML='<p><strong>Plugin server not reachable</strong></p><p>Make sure your plugin dev server is running.</p>'}})});consoleToggle.addEventListener('click',function(){consoleBody.classList.toggle('open')});reloadBtn.addEventListener('click',function(){if(cachedConfig)iframe.src=cachedConfig.pluginUrl});themeToggle.addEventListener('click',function(){currentTheme=currentTheme==='dark'?'light':'dark';uTB();sTP({type:'theme_update',messageId:'t_'+Date.now(),payload:gT()})});modeBadge.addEventListener('click',function(){if(currentMode==='mock'){if(!hasCredentials){sLM();return}sM('live')}else sM('mock')});envSelector.addEventListener('change',function(){fetch('/api/environment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({environment:envSelector.value})}).then(function(r){return r.json()}).then(function(d){currentEnvironment=d.environment;currentMode=d.mode;hasCredentials=d.hasCredentials;uMB();if(currentMode==='live'){creditBalance.style.display='inline';fC()}else{creditBalance.style.display='none';creditBalance.textContent=''}lM('info','Environment: '+currentEnvironment.toUpperCase()+(hasCredentials?'':' (not authenticated)'))}).catch(function(e){lM('error',e.message)})});function sM(m){fetch('/api/mode',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:m})}).then(function(r){if(!r.ok)return r.json().then(function(e){throw new Error(e.error)});return r.json()}).then(function(d){currentMode=d.mode;uMB();if(currentMode==='live'){creditBalance.style.display='inline';fC()}else{creditBalance.style.display='none';creditBalance.textContent=''}}).catch(function(e){lM('error',e.message)})}function sLM(){loginModal.style.display='flex';loginEmail.value='';loginPassword.value='';loginError.style.display='none';loginSubmit.disabled=false;loginSubmit.textContent='Sign in';loginTarget.textContent=currentEnvironment==='production'?'fias.io':'staging.fias.io';loginEmail.focus()}function hLM(){loginModal.style.display='none'}loginCancel.addEventListener('click',hLM);loginModal.addEventListener('click',function(e){if(e.target===loginModal)hLM()});loginEmail.addEventListener('keydown',function(e){if(e.key==='Enter')loginPassword.focus();if(e.key==='Escape')hLM()});loginPassword.addEventListener('keydown',function(e){if(e.key==='Enter')doLogin();if(e.key==='Escape')hLM()});loginSubmit.addEventListener('click',doLogin);function doLogin(){var em=loginEmail.value.trim(),pw=loginPassword.value;if(!em||!pw){loginError.textContent='Email and password are required.';loginError.style.display='block';return}loginSubmit.disabled=true;loginSubmit.textContent='Signing in...';loginError.style.display='none';fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:em,password:pw})}).then(function(r){if(!r.ok)return r.json().then(function(e){throw new Error(e.error||'Sign in failed')});return r.json()}).then(function(){hasCredentials=true;hLM();lM('info','Signed in for '+currentEnvironment.toUpperCase());sM('live')}).catch(function(e){loginError.textContent=e.message;loginError.style.display='block'}).finally(function(){loginSubmit.disabled=false;loginSubmit.textContent='Sign in'})}window.addEventListener('message',function(ev){if(ev.source!==iframe.contentWindow)return;var d=ev.data;if(!d||typeof d!=='object'||!d.type||!d.messageId)return;lM('recv',d.type,d.payload);if(d.type==='ready')return;if(d.type==='resize'){var h=d.payload&&d.payload.height;if(typeof h==='number'&&h>0){iframe.style.height=h+'px';iframe.style.flex='none'}return}if(d.type==='toast'){var msg=d.payload&&d.payload.message;if(typeof msg==='string')lM('toast',msg);return}if(d.type==='navigate'){var p=d.payload&&d.payload.path;if(typeof p==='string')lM('nav',p);return}hR(d)});function hR(d){try{var rp=PM[d.type];if(rp&&cachedConfig&&cachedConfig.permissions.indexOf(rp)===-1)throw new Error('Permission denied: '+rp);cRL(d.type)}catch(e){lM('error',e.message);sTP({type:'response',messageId:d.messageId,payload:null,error:e.message});return}fetch('/api/bridge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:d.type,payload:d.payload})}).then(function(r){if(!r.ok)return r.json().then(function(e){throw new Error(e.error||'Bridge call failed')});return r.json()}).then(function(res){lM('send','response',res);if(d.type==='entity_invoke'&&res.metadata&&res.metadata.cost>0){lM('cost','Credits: '+res.metadata.cost.toFixed(4));fC()}sTP({type:'response',messageId:d.messageId,payload:res})}).catch(function(e){lM('error',e.message);sTP({type:'response',messageId:d.messageId,payload:null,error:e.message})})}iframe.addEventListener('load',function(){if(!cachedConfig)return;sTP({type:'init',messageId:'init_0',payload:{archId:'dev_harness',permissions:cachedConfig.permissions,theme:gT(),currentPath:'/'}});lM('send','init')});function uMB(){modeBadge.textContent=currentMode.toUpperCase()+' \\u21C6';modeBadge.className='mode-badge '+(currentMode==='live'?'mode-live':'mode-mock');modeBadge.title='Click to switch to '+(currentMode==='live'?'Mock':'Live')+' mode'}function uTB(){themeBadge.textContent=currentTheme.toUpperCase();themeBadge.className='theme-badge theme-'+currentTheme;document.body.style.background=currentTheme==='light'?'#fff':'#0a0a0a'}function sTP(m){iframe.contentWindow&&iframe.contentWindow.postMessage(m,'*')}function cRL(t){var l=RL[t];if(!l)return;var n=Date.now(),b=rb[t];if(!b||n-b.s>60000){rb[t]={c:1,s:n};return}if(b.c>=l.m)throw new Error('Rate limit exceeded for '+t);b.c++}function gT(){if(currentTheme==='light')return{mode:'light',colors:{primary:'#171717',secondary:'#e5e5e5',background:'#fff',surface:'#fafafa',text:'#0a0a0a',textSecondary:'#737373',border:'#e5e5e5',error:'#dc2626',warning:'#d97706',success:'#16a34a'},spacing:SP,fonts:FN};return{mode:'dark',colors:{primary:'#fff',secondary:'#1f1f1f',background:'#0a0a0a',surface:'#171717',text:'#fff',textSecondary:'#a6a6a6',border:'#2e2e2e',error:'#ef4444',warning:'#f59e0b',success:'#22c55e'},spacing:SP,fonts:FN}}function cPR(cb){fetch('/api/check-plugin').then(function(r){return r.json()}).then(function(d){cb(d.reachable)}).catch(function(){cb(true)})}function fC(){fetch('/api/credits').then(function(r){return r.json()}).then(function(d){if(d.balance!=null&&isFinite(d.balance))creditBalance.textContent='Credits: '+d.balance.toFixed(2)}).catch(function(){})}function lM(dir,type,payload){messageCount++;consoleCount.textContent=messageCount+' messages';var e=document.createElement('div');e.className='log-entry';var cls='log-info';if(dir==='error')cls='log-error';if(dir==='warn'||dir==='cost'||dir==='toast')cls='log-warn';var t='<span class="log-time">'+new Date().toLocaleTimeString()+'</span><span class="'+cls+'">['+dir.toUpperCase()+'] '+type+'</span>';if(payload&&typeof payload==='object')t+=' <span style="color:#6b7280">'+JSON.stringify(payload).substring(0,120)+'</span>';e.innerHTML=t;consoleBody.appendChild(e);consoleBody.scrollTop=consoleBody.scrollHeight}})();`;
695
434
  //# sourceMappingURL=harness-server.js.map