@fias/plugin-dev-harness 1.4.1 → 1.5.0

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.
@@ -1,9 +1,6 @@
1
1
  /**
2
2
  * Fias Arche Dev — Client-side Bridge Host
3
3
  *
4
- * Ported from the production PluginBridgeHost
5
- * (client/src/arche/components/PluginBridge.ts).
6
- *
7
4
  * Handles postMessage communication with the plugin iframe,
8
5
  * enforcing permissions and rate limits, and proxying server-side
9
6
  * operations through the harness Express server.
@@ -22,14 +19,17 @@
22
19
  var pluginStatus = document.getElementById('plugin-status');
23
20
  var themeBadge = document.getElementById('theme-badge');
24
21
  var loginModal = document.getElementById('login-modal');
25
- var loginInput = document.getElementById('login-input');
22
+ var loginEmail = document.getElementById('login-email');
23
+ var loginPassword = document.getElementById('login-password');
26
24
  var loginError = document.getElementById('login-error');
27
25
  var loginSubmit = document.getElementById('login-submit');
28
26
  var loginCancel = document.getElementById('login-cancel');
27
+ var envSelector = document.getElementById('env-selector');
29
28
 
30
29
  var messageCount = 0;
31
30
  var currentTheme = 'dark';
32
31
  var currentMode = 'mock';
32
+ var currentEnvironment = 'staging';
33
33
  var hasCredentials = false;
34
34
  var cachedConfig = null;
35
35
 
@@ -62,8 +62,10 @@
62
62
  cachedConfig = config;
63
63
  currentTheme = config.mockTheme || 'dark';
64
64
  currentMode = config.mode || 'mock';
65
+ currentEnvironment = config.environment || 'staging';
65
66
  hasCredentials = config.hasCredentials || false;
66
67
 
68
+ envSelector.value = currentEnvironment;
67
69
  updateThemeBadge();
68
70
  updateModeBadge();
69
71
 
@@ -72,7 +74,6 @@
72
74
  fetchCredits();
73
75
  }
74
76
 
75
- // Check if plugin server is reachable, then load
76
77
  checkPluginReachable(config.pluginUrl, function (reachable) {
77
78
  if (reachable) {
78
79
  pluginStatus.classList.add('hidden');
@@ -94,7 +95,6 @@
94
95
 
95
96
  reloadBtn.addEventListener('click', function () {
96
97
  if (cachedConfig) {
97
- // Re-check reachability on reload
98
98
  pluginStatus.classList.remove('hidden', 'error');
99
99
  pluginStatus.innerHTML =
100
100
  '<div class="status-spinner"></div><p>Connecting to plugin server...</p>';
@@ -124,10 +124,9 @@
124
124
  logMessage('send', 'theme_update', { mode: currentTheme });
125
125
  });
126
126
 
127
- // Mode toggle — click the badge to switch between mock/live
127
+ // Mode toggle
128
128
  modeBadge.addEventListener('click', function () {
129
129
  if (currentMode === 'mock') {
130
- // Switching to live — need credentials
131
130
  if (!hasCredentials) {
132
131
  showLoginModal();
133
132
  return;
@@ -138,6 +137,42 @@
138
137
  }
139
138
  });
140
139
 
140
+ // Environment selector
141
+ envSelector.addEventListener('change', function () {
142
+ fetch('/api/environment', {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({ environment: envSelector.value }),
146
+ })
147
+ .then(function (r) {
148
+ return r.json();
149
+ })
150
+ .then(function (data) {
151
+ currentEnvironment = data.environment;
152
+ currentMode = data.mode;
153
+ hasCredentials = data.hasCredentials;
154
+ updateModeBadge();
155
+
156
+ if (currentMode === 'live') {
157
+ creditBalance.style.display = 'inline';
158
+ fetchCredits();
159
+ } else {
160
+ creditBalance.style.display = 'none';
161
+ creditBalance.textContent = '';
162
+ }
163
+
164
+ logMessage(
165
+ 'info',
166
+ 'Environment: ' +
167
+ currentEnvironment.toUpperCase() +
168
+ (hasCredentials ? '' : ' (not authenticated)'),
169
+ );
170
+ })
171
+ .catch(function (err) {
172
+ logMessage('error', err.message);
173
+ });
174
+ });
175
+
141
176
  function switchMode(newMode) {
142
177
  fetch('/api/mode', {
143
178
  method: 'POST',
@@ -174,17 +209,21 @@
174
209
  // Login Modal
175
210
  // ────────────────────────────────────────────────────────────────
176
211
 
212
+ var loginTarget = document.getElementById('login-target');
213
+
177
214
  function showLoginModal() {
178
215
  loginModal.style.display = 'flex';
179
- loginInput.value = '';
216
+ loginEmail.value = '';
217
+ loginPassword.value = '';
180
218
  loginError.style.display = 'none';
181
- loginInput.focus();
219
+ loginSubmit.disabled = false;
220
+ loginSubmit.textContent = 'Sign in';
221
+ loginTarget.textContent = currentEnvironment === 'production' ? 'fias.io' : 'staging.fias.io';
222
+ loginEmail.focus();
182
223
  }
183
224
 
184
225
  function hideLoginModal() {
185
226
  loginModal.style.display = 'none';
186
- loginInput.value = '';
187
- loginError.style.display = 'none';
188
227
  }
189
228
 
190
229
  loginCancel.addEventListener('click', hideLoginModal);
@@ -193,7 +232,12 @@
193
232
  if (e.target === loginModal) hideLoginModal();
194
233
  });
195
234
 
196
- loginInput.addEventListener('keydown', function (e) {
235
+ loginEmail.addEventListener('keydown', function (e) {
236
+ if (e.key === 'Enter') loginPassword.focus();
237
+ if (e.key === 'Escape') hideLoginModal();
238
+ });
239
+
240
+ loginPassword.addEventListener('keydown', function (e) {
197
241
  if (e.key === 'Enter') submitLogin();
198
242
  if (e.key === 'Escape') hideLoginModal();
199
243
  });
@@ -201,25 +245,28 @@
201
245
  loginSubmit.addEventListener('click', submitLogin);
202
246
 
203
247
  function submitLogin() {
204
- var apiKey = loginInput.value.trim();
205
- if (!apiKey) {
206
- loginError.textContent = 'Please enter an API key.';
248
+ var email = loginEmail.value.trim();
249
+ var password = loginPassword.value;
250
+
251
+ if (!email || !password) {
252
+ loginError.textContent = 'Email and password are required.';
207
253
  loginError.style.display = 'block';
208
254
  return;
209
255
  }
210
256
 
211
257
  loginSubmit.disabled = true;
212
- loginSubmit.textContent = 'Connecting...';
258
+ loginSubmit.textContent = 'Signing in...';
259
+ loginError.style.display = 'none';
213
260
 
214
- fetch('/api/login', {
261
+ fetch('/api/auth/login', {
215
262
  method: 'POST',
216
263
  headers: { 'Content-Type': 'application/json' },
217
- body: JSON.stringify({ apiKey: apiKey }),
264
+ body: JSON.stringify({ email: email, password: password }),
218
265
  })
219
266
  .then(function (r) {
220
267
  if (!r.ok) {
221
268
  return r.json().then(function (err) {
222
- throw new Error(err.error || 'Login failed');
269
+ throw new Error(err.error || 'Sign in failed');
223
270
  });
224
271
  }
225
272
  return r.json();
@@ -227,8 +274,7 @@
227
274
  .then(function () {
228
275
  hasCredentials = true;
229
276
  hideLoginModal();
230
- logMessage('info', 'API key saved successfully');
231
- // Automatically switch to live mode after login
277
+ logMessage('info', 'Signed in for ' + currentEnvironment.toUpperCase());
232
278
  switchMode('live');
233
279
  })
234
280
  .catch(function (err) {
@@ -237,7 +283,7 @@
237
283
  })
238
284
  .finally(function () {
239
285
  loginSubmit.disabled = false;
240
- loginSubmit.textContent = 'Save & Connect';
286
+ loginSubmit.textContent = 'Sign in';
241
287
  });
242
288
  }
243
289
 
@@ -253,7 +299,6 @@
253
299
 
254
300
  logMessage('recv', data.type, data.payload);
255
301
 
256
- // Fire-and-forget messages
257
302
  if (data.type === 'ready') return;
258
303
 
259
304
  if (data.type === 'resize') {
@@ -281,19 +326,15 @@
281
326
  return;
282
327
  }
283
328
 
284
- // Request/response messages — enforce permissions + rate limits, then proxy
285
329
  handleRequest(data);
286
330
  });
287
331
 
288
332
  function handleRequest(data) {
289
333
  try {
290
- // Check permissions
291
334
  var requiredPerm = PERMISSION_MAP[data.type];
292
335
  if (requiredPerm && cachedConfig && cachedConfig.permissions.indexOf(requiredPerm) === -1) {
293
336
  throw new Error('Permission denied: ' + requiredPerm + ' not granted');
294
337
  }
295
-
296
- // Check rate limit
297
338
  checkRateLimit(data.type);
298
339
  } catch (err) {
299
340
  logMessage('error', err.message);
@@ -306,7 +347,12 @@
306
347
  return;
307
348
  }
308
349
 
309
- // Proxy to harness server
350
+ // Streaming path for entity_invoke with stream: true
351
+ if (data.type === 'entity_invoke' && data.payload && data.payload.stream) {
352
+ handleStreamingRequest(data);
353
+ return;
354
+ }
355
+
310
356
  fetch('/api/bridge', {
311
357
  method: 'POST',
312
358
  headers: { 'Content-Type': 'application/json' },
@@ -323,7 +369,6 @@
323
369
  .then(function (result) {
324
370
  logMessage('send', 'response', result);
325
371
 
326
- // Show cost for entity invocations in live mode
327
372
  if (
328
373
  data.type === 'entity_invoke' &&
329
374
  result.metadata &&
@@ -350,6 +395,86 @@
350
395
  });
351
396
  }
352
397
 
398
+ function handleStreamingRequest(data) {
399
+ fetch('/api/bridge/stream', {
400
+ method: 'POST',
401
+ headers: { 'Content-Type': 'application/json' },
402
+ body: JSON.stringify({ type: data.type, payload: data.payload }),
403
+ })
404
+ .then(function (response) {
405
+ if (!response.ok) {
406
+ return response.json().then(function (err) {
407
+ throw new Error(err.error || 'Bridge streaming call failed');
408
+ });
409
+ }
410
+
411
+ var contentType = response.headers.get('content-type') || '';
412
+ if (contentType.indexOf('text/event-stream') === -1) {
413
+ // Fallback: not SSE, parse as JSON
414
+ return response.json().then(function (result) {
415
+ sendToPlugin({ type: 'response', messageId: data.messageId, payload: result });
416
+ });
417
+ }
418
+
419
+ // Parse SSE stream
420
+ var reader = response.body.getReader();
421
+ var decoder = new TextDecoder();
422
+ var buffer = '';
423
+ var finalPayload = null;
424
+
425
+ function pump() {
426
+ return reader.read().then(function (chunk) {
427
+ if (chunk.done) {
428
+ if (finalPayload) {
429
+ logMessage('send', 'response', finalPayload);
430
+ sendToPlugin({ type: 'response', messageId: data.messageId, payload: finalPayload });
431
+ }
432
+ return;
433
+ }
434
+
435
+ buffer += decoder.decode(chunk.value, { stream: true });
436
+ var lines = buffer.split('\n');
437
+ buffer = lines.pop() || '';
438
+
439
+ for (var i = 0; i < lines.length; i++) {
440
+ var line = lines[i];
441
+ if (line.indexOf('data: ') !== 0) continue;
442
+ try {
443
+ var parsed = JSON.parse(line.slice(6));
444
+ if (parsed.error) throw new Error(parsed.error);
445
+ if (parsed.text) {
446
+ sendToPlugin({ type: 'stream_token', messageId: data.messageId, text: parsed.text });
447
+ }
448
+ if (parsed.done) {
449
+ finalPayload = { output: parsed.output, metadata: parsed.metadata };
450
+ if (parsed.metadata && parsed.metadata.cost > 0) {
451
+ logMessage('cost', 'Credits used: ' + parsed.metadata.cost.toFixed(4));
452
+ fetchCredits();
453
+ }
454
+ }
455
+ } catch (e) {
456
+ if (e instanceof SyntaxError) continue;
457
+ throw e;
458
+ }
459
+ }
460
+
461
+ return pump();
462
+ });
463
+ }
464
+
465
+ return pump();
466
+ })
467
+ .catch(function (err) {
468
+ logMessage('error', err.message);
469
+ sendToPlugin({
470
+ type: 'response',
471
+ messageId: data.messageId,
472
+ payload: null,
473
+ error: err.message,
474
+ });
475
+ });
476
+ }
477
+
353
478
  // ────────────────────────────────────────────────────────────────
354
479
  // Iframe Init
355
480
  // ────────────────────────────────────────────────────────────────
@@ -385,11 +510,26 @@
385
510
  themeBadge.textContent = currentTheme.toUpperCase();
386
511
  themeBadge.className = 'theme-badge theme-' + currentTheme;
387
512
  document.body.style.background = currentTheme === 'light' ? '#ffffff' : '#0a0a0a';
513
+
514
+ // Update toolbar to match theme
515
+ var toolbar = document.querySelector('.toolbar');
516
+ var logo = document.querySelector('.logo');
517
+ var icons = document.querySelectorAll('.btn-icon');
518
+ var env = document.getElementById('env-selector');
519
+ if (currentTheme === 'light') {
520
+ if (toolbar) { toolbar.style.background = '#f5f5f5'; toolbar.style.borderBottomColor = '#e5e5e5'; }
521
+ if (logo) { logo.style.color = '#171717'; }
522
+ if (env) { env.style.background = '#e5e5e5'; env.style.color = '#171717'; env.style.borderColor = '#d4d4d4'; }
523
+ icons.forEach(function (btn) { btn.style.background = '#e5e5e5'; btn.style.color = '#171717'; btn.style.borderColor = '#d4d4d4'; });
524
+ } else {
525
+ if (toolbar) { toolbar.style.background = '#18181b'; toolbar.style.borderBottomColor = '#3f3f46'; }
526
+ if (logo) { logo.style.color = '#e4e4e7'; }
527
+ if (env) { env.style.background = '#27272a'; env.style.color = '#a1a1aa'; env.style.borderColor = '#3f3f46'; }
528
+ icons.forEach(function (btn) { btn.style.background = '#27272a'; btn.style.color = '#e4e4e7'; btn.style.borderColor = '#3f3f46'; });
529
+ }
388
530
  }
389
531
 
390
532
  function sendToPlugin(message) {
391
- // targetOrigin '*' because the plugin may be on a different port.
392
- // Security is enforced by checking event.source on incoming messages.
393
533
  iframe.contentWindow && iframe.contentWindow.postMessage(message, '*');
394
534
  }
395
535
 
@@ -419,32 +559,51 @@
419
559
  var SPACING = { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' };
420
560
  var FONTS = { body: SYSTEM_FONTS, heading: SYSTEM_FONTS, mono: MONO_FONTS };
421
561
 
562
+ var COMPONENTS = {
563
+ borderRadius: '0.5rem',
564
+ buttonRadius: '0.375rem',
565
+ cardRadius: '0.5rem',
566
+ inputRadius: '0.375rem',
567
+ shadowSm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
568
+ shadowMd: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
569
+ shadowLg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
570
+ borderWidth: '1px',
571
+ };
572
+
422
573
  function getTheme() {
423
574
  if (currentTheme === 'light') {
424
575
  return {
425
576
  mode: 'light',
426
577
  colors: {
427
- primary: '#171717', secondary: '#e5e5e5',
578
+ primary: '#171717', primaryText: '#ffffff',
579
+ secondary: '#e5e5e5', accent: '#2563eb',
428
580
  background: '#ffffff', surface: '#fafafa',
581
+ card: '#ffffff', cardText: '#0a0a0a',
429
582
  text: '#0a0a0a', textSecondary: '#737373',
583
+ muted: '#f5f5f5', mutedText: '#a3a3a3',
430
584
  border: '#e5e5e5',
431
- error: '#dc2626', warning: '#d97706', success: '#16a34a',
585
+ error: '#dc2626', warning: '#d97706', success: '#16a34a', info: '#2563eb',
432
586
  },
433
587
  spacing: SPACING,
434
588
  fonts: FONTS,
589
+ components: COMPONENTS,
435
590
  };
436
591
  }
437
592
  return {
438
593
  mode: 'dark',
439
594
  colors: {
440
- primary: '#ffffff', secondary: '#1f1f1f',
595
+ primary: '#ffffff', primaryText: '#0a0a0a',
596
+ secondary: '#1f1f1f', accent: '#3b82f6',
441
597
  background: '#0a0a0a', surface: '#171717',
598
+ card: '#141414', cardText: '#ffffff',
442
599
  text: '#ffffff', textSecondary: '#a6a6a6',
600
+ muted: '#1e1e1e', mutedText: '#737373',
443
601
  border: '#2e2e2e',
444
- error: '#ef4444', warning: '#f59e0b', success: '#22c55e',
602
+ error: '#ef4444', warning: '#f59e0b', success: '#22c55e', info: '#3b82f6',
445
603
  },
446
604
  spacing: SPACING,
447
605
  fonts: FONTS,
606
+ components: COMPONENTS,
448
607
  };
449
608
  }
450
609
 
@@ -510,7 +669,6 @@
510
669
  function checkPluginReachable(url, callback) {
511
670
  var done = false;
512
671
 
513
- // Try fetching through the harness server to avoid CORS issues
514
672
  fetch('/api/check-plugin')
515
673
  .then(function (r) { return r.json(); })
516
674
  .then(function (data) {
@@ -522,12 +680,10 @@
522
680
  .catch(function () {
523
681
  if (!done) {
524
682
  done = true;
525
- // If the check endpoint doesn't exist, assume reachable and let iframe handle it
526
683
  callback(true);
527
684
  }
528
685
  });
529
686
 
530
- // Timeout after 5 seconds
531
687
  setTimeout(function () {
532
688
  if (!done) {
533
689
  done = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fias/plugin-dev-harness",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Development harness for building and testing FIAS plugin arches locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",