@fias/plugin-dev-harness 1.5.3 → 1.5.4

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,1003 +0,0 @@
1
- /**
2
- * Fias Arche Dev — Client-side Bridge Host
3
- *
4
- * Handles postMessage communication with the plugin iframe,
5
- * enforcing permissions and rate limits, and proxying server-side
6
- * operations through the harness Express server.
7
- */
8
- (function () {
9
- 'use strict';
10
-
11
- var iframe = document.getElementById('plugin-iframe');
12
- var consoleBody = document.getElementById('console-body');
13
- var consoleCount = document.getElementById('console-count');
14
- var consoleToggle = document.getElementById('console-toggle');
15
- var creditBalance = document.getElementById('credit-balance');
16
- var themeToggle = document.getElementById('theme-toggle');
17
- var reloadBtn = document.getElementById('reload-btn');
18
- var modeBadge = document.getElementById('mode-badge');
19
- var modeToggle = document.getElementById('mode-toggle');
20
- var pluginStatus = document.getElementById('plugin-status');
21
- var themeBadge = document.getElementById('theme-badge');
22
- var envSelector = document.getElementById('env-selector');
23
-
24
- var messageCount = 0;
25
- var currentTheme = 'dark';
26
- var currentMode = 'mock';
27
- var currentEnvironment = 'staging';
28
- var hasCredentials = false;
29
- var cachedConfig = null;
30
-
31
- /** Permission requirements per bridge call type (matches production) */
32
- var PERMISSION_MAP = {
33
- get_user: 'user:profile:read',
34
- get_theme: 'theme:read',
35
- entity_invoke: 'entities:invoke',
36
- image_generate: 'entities:image_generate',
37
- storage_read: 'storage:sandbox',
38
- storage_write: 'storage:sandbox',
39
- storage_list: 'storage:sandbox',
40
- storage_delete: 'storage:sandbox',
41
- };
42
-
43
- /** Rate limits per message type (matches production) */
44
- var RATE_LIMITS = {
45
- entity_invoke: { maxPerMinute: 60 },
46
- image_generate: { maxPerMinute: 10 },
47
- storage_write: { maxPerMinute: 120 },
48
- storage_read: { maxPerMinute: 300 },
49
- storage_list: { maxPerMinute: 60 },
50
- storage_delete: { maxPerMinute: 60 },
51
- };
52
- var rateBuckets = {};
53
-
54
- // ────────────────────────────────────────────────────────────────
55
- // Initialization
56
- // ────────────────────────────────────────────────────────────────
57
-
58
- fetchConfig().then(function (config) {
59
- cachedConfig = config;
60
- currentTheme = config.mockTheme || 'dark';
61
- currentMode = config.mode || 'mock';
62
- currentEnvironment = config.environment || 'staging';
63
- hasCredentials = config.hasCredentials || false;
64
-
65
- envSelector.value = currentEnvironment;
66
- updateThemeBadge();
67
- updateModeBadge();
68
-
69
- if (currentMode === 'live') {
70
- creditBalance.style.display = 'inline';
71
- fetchCredits();
72
- }
73
-
74
- checkPluginReachable(config.pluginUrl, function (reachable) {
75
- if (reachable) {
76
- pluginStatus.classList.add('hidden');
77
- iframe.classList.remove('hidden');
78
- iframe.src = config.pluginUrl;
79
- } else {
80
- showPluginError(config.pluginUrl);
81
- }
82
- });
83
- });
84
-
85
- // ────────────────────────────────────────────────────────────────
86
- // UI Controls
87
- // ────────────────────────────────────────────────────────────────
88
-
89
- consoleToggle.addEventListener('click', function () {
90
- consoleBody.classList.toggle('open');
91
- });
92
-
93
- reloadBtn.addEventListener('click', function () {
94
- if (cachedConfig) {
95
- pluginStatus.classList.remove('hidden', 'error');
96
- pluginStatus.innerHTML =
97
- '<div class="status-spinner"></div><p>Connecting to plugin server...</p>';
98
- iframe.classList.add('hidden');
99
- checkPluginReachable(cachedConfig.pluginUrl, function (reachable) {
100
- if (reachable) {
101
- pluginStatus.classList.add('hidden');
102
- iframe.classList.remove('hidden');
103
- iframe.src = cachedConfig.pluginUrl;
104
- } else {
105
- showPluginError(cachedConfig.pluginUrl);
106
- }
107
- });
108
- } else {
109
- iframe.src = iframe.src;
110
- }
111
- });
112
-
113
- themeToggle.addEventListener('click', function () {
114
- currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
115
- updateThemeBadge();
116
- sendToPlugin({
117
- type: 'theme_update',
118
- messageId: 'theme_' + Date.now(),
119
- payload: getTheme(),
120
- });
121
- logMessage('send', 'theme_update', { mode: currentTheme });
122
- });
123
-
124
- // Mode toggle button
125
- modeToggle.addEventListener('click', function () {
126
- if (currentMode === 'mock') {
127
- if (!hasCredentials) {
128
- logMessage('error', 'Not authenticated. Run: npx fias-dev login');
129
- return;
130
- }
131
- switchMode('live');
132
- } else {
133
- switchMode('mock');
134
- }
135
- });
136
-
137
- // Environment selector
138
- envSelector.addEventListener('change', function () {
139
- fetch('/api/environment', {
140
- method: 'POST',
141
- headers: { 'Content-Type': 'application/json' },
142
- body: JSON.stringify({ environment: envSelector.value }),
143
- })
144
- .then(function (r) {
145
- return r.json();
146
- })
147
- .then(function (data) {
148
- currentEnvironment = data.environment;
149
- currentMode = data.mode;
150
- hasCredentials = data.hasCredentials;
151
- updateModeBadge();
152
-
153
- if (currentMode === 'live') {
154
- creditBalance.style.display = 'inline';
155
- fetchCredits();
156
- } else {
157
- creditBalance.style.display = 'none';
158
- creditBalance.textContent = '';
159
- }
160
-
161
- logMessage(
162
- 'info',
163
- 'Environment: ' +
164
- currentEnvironment.toUpperCase() +
165
- (hasCredentials ? '' : ' (not authenticated)'),
166
- );
167
- })
168
- .catch(function (err) {
169
- logMessage('error', err.message);
170
- });
171
- });
172
-
173
- function switchMode(newMode) {
174
- fetch('/api/mode', {
175
- method: 'POST',
176
- headers: { 'Content-Type': 'application/json' },
177
- body: JSON.stringify({ mode: newMode }),
178
- })
179
- .then(function (r) {
180
- if (!r.ok) {
181
- return r.json().then(function (err) {
182
- throw new Error(err.error || 'Failed to switch mode');
183
- });
184
- }
185
- return r.json();
186
- })
187
- .then(function (data) {
188
- currentMode = data.mode;
189
- updateModeBadge();
190
- logMessage('info', 'Mode switched to ' + currentMode.toUpperCase());
191
-
192
- if (currentMode === 'live') {
193
- creditBalance.style.display = 'inline';
194
- fetchCredits();
195
- } else {
196
- creditBalance.style.display = 'none';
197
- creditBalance.textContent = '';
198
- }
199
- })
200
- .catch(function (err) {
201
- logMessage('error', err.message);
202
- });
203
- }
204
-
205
- // ────────────────────────────────────────────────────────────────
206
- // Message Handling
207
- // ────────────────────────────────────────────────────────────────
208
-
209
- window.addEventListener('message', function (event) {
210
- if (event.source !== iframe.contentWindow) return;
211
-
212
- var data = event.data;
213
- if (!data || typeof data !== 'object' || !data.type || !data.messageId) return;
214
-
215
- logMessage('recv', data.type, data.payload);
216
-
217
- if (data.type === 'ready') return;
218
-
219
- if (data.type === 'resize') {
220
- var height = data.payload && data.payload.height;
221
- if (typeof height === 'number' && height > 0) {
222
- iframe.style.height = height + 'px';
223
- iframe.style.flex = 'none';
224
- }
225
- return;
226
- }
227
-
228
- if (data.type === 'toast') {
229
- var msg = data.payload && data.payload.message;
230
- if (typeof msg === 'string') {
231
- logMessage('toast', msg, data.payload);
232
- }
233
- return;
234
- }
235
-
236
- if (data.type === 'navigate') {
237
- var navPath = data.payload && data.payload.path;
238
- if (typeof navPath === 'string') {
239
- logMessage('nav', navPath);
240
- }
241
- return;
242
- }
243
-
244
- handleRequest(data);
245
- });
246
-
247
- function handleRequest(data) {
248
- try {
249
- var requiredPerm = PERMISSION_MAP[data.type];
250
- if (requiredPerm && cachedConfig && cachedConfig.permissions.indexOf(requiredPerm) === -1) {
251
- throw new Error('Permission denied: ' + requiredPerm + ' not granted');
252
- }
253
- checkRateLimit(data.type);
254
- } catch (err) {
255
- logMessage('error', err.message);
256
- sendToPlugin({
257
- type: 'response',
258
- messageId: data.messageId,
259
- payload: null,
260
- error: err.message,
261
- });
262
- return;
263
- }
264
-
265
- // Streaming path for entity_invoke with stream: true
266
- if (data.type === 'entity_invoke' && data.payload && data.payload.stream) {
267
- handleStreamingRequest(data);
268
- return;
269
- }
270
-
271
- fetch('/api/bridge', {
272
- method: 'POST',
273
- headers: { 'Content-Type': 'application/json' },
274
- body: JSON.stringify({ type: data.type, payload: data.payload }),
275
- })
276
- .then(function (response) {
277
- if (!response.ok) {
278
- return response.json().then(function (err) {
279
- throw new Error(err.error || 'Bridge call failed');
280
- });
281
- }
282
- return response.json();
283
- })
284
- .then(function (result) {
285
- logMessage('send', 'response', result);
286
-
287
- if (
288
- data.type === 'entity_invoke' &&
289
- result.metadata &&
290
- result.metadata.cost > 0
291
- ) {
292
- logMessage('cost', 'Credits used: ' + result.metadata.cost.toFixed(4));
293
- fetchCredits();
294
- }
295
-
296
- sendToPlugin({
297
- type: 'response',
298
- messageId: data.messageId,
299
- payload: result,
300
- });
301
- })
302
- .catch(function (err) {
303
- logMessage('error', err.message);
304
- sendToPlugin({
305
- type: 'response',
306
- messageId: data.messageId,
307
- payload: null,
308
- error: err.message,
309
- });
310
- });
311
- }
312
-
313
- function handleStreamingRequest(data) {
314
- fetch('/api/bridge/stream', {
315
- method: 'POST',
316
- headers: { 'Content-Type': 'application/json' },
317
- body: JSON.stringify({ type: data.type, payload: data.payload }),
318
- })
319
- .then(function (response) {
320
- if (!response.ok) {
321
- return response.json().then(function (err) {
322
- throw new Error(err.error || 'Bridge streaming call failed');
323
- });
324
- }
325
-
326
- var contentType = response.headers.get('content-type') || '';
327
- if (contentType.indexOf('text/event-stream') === -1) {
328
- // Fallback: not SSE, parse as JSON
329
- return response.json().then(function (result) {
330
- sendToPlugin({ type: 'response', messageId: data.messageId, payload: result });
331
- });
332
- }
333
-
334
- // Parse SSE stream
335
- var reader = response.body.getReader();
336
- var decoder = new TextDecoder();
337
- var buffer = '';
338
- var finalPayload = null;
339
-
340
- function pump() {
341
- return reader.read().then(function (chunk) {
342
- if (chunk.done) {
343
- if (finalPayload) {
344
- logMessage('send', 'response', finalPayload);
345
- sendToPlugin({ type: 'response', messageId: data.messageId, payload: finalPayload });
346
- }
347
- return;
348
- }
349
-
350
- buffer += decoder.decode(chunk.value, { stream: true });
351
- var lines = buffer.split('\n');
352
- buffer = lines.pop() || '';
353
-
354
- for (var i = 0; i < lines.length; i++) {
355
- var line = lines[i];
356
- if (line.indexOf('data: ') !== 0) continue;
357
- try {
358
- var parsed = JSON.parse(line.slice(6));
359
- if (parsed.error) throw new Error(parsed.error);
360
- if (parsed.text) {
361
- sendToPlugin({ type: 'stream_token', messageId: data.messageId, text: parsed.text });
362
- }
363
- if (parsed.done) {
364
- finalPayload = { output: parsed.output, metadata: parsed.metadata };
365
- if (parsed.metadata && parsed.metadata.cost > 0) {
366
- logMessage('cost', 'Credits used: ' + parsed.metadata.cost.toFixed(4));
367
- fetchCredits();
368
- }
369
- }
370
- } catch (e) {
371
- if (e instanceof SyntaxError) continue;
372
- throw e;
373
- }
374
- }
375
-
376
- return pump();
377
- });
378
- }
379
-
380
- return pump();
381
- })
382
- .catch(function (err) {
383
- logMessage('error', err.message);
384
- sendToPlugin({
385
- type: 'response',
386
- messageId: data.messageId,
387
- payload: null,
388
- error: err.message,
389
- });
390
- });
391
- }
392
-
393
- // ────────────────────────────────────────────────────────────────
394
- // Publish Wizard
395
- // ────────────────────────────────────────────────────────────────
396
-
397
- var publishBtn = document.getElementById('publish-btn');
398
- var publishModal = document.getElementById('publish-modal');
399
- var pubCancel = document.getElementById('pub-cancel');
400
- var pubBack = document.getElementById('pub-back');
401
- var pubNext = document.getElementById('pub-next');
402
- var pubError = document.getElementById('pub-error');
403
- var pubStep = 1;
404
- var pubManifest = null;
405
- var pubSubmissionId = null;
406
- var pubPollTimer = null;
407
- var pubValidationPassed = false;
408
-
409
- publishBtn.addEventListener('click', function () {
410
- if (currentMode !== 'live' || !hasCredentials) {
411
- pubError.textContent = 'Switch to live mode and sign in to publish.';
412
- pubError.style.display = 'block';
413
- publishModal.style.display = 'flex';
414
- showPublishStep(1);
415
- return;
416
- }
417
- pubError.style.display = 'none';
418
- fetch('/api/manifest')
419
- .then(function (r) {
420
- if (!r.ok) throw new Error('fias-plugin.json not found');
421
- return r.json();
422
- })
423
- .then(function (manifest) {
424
- pubManifest = manifest;
425
- populatePublishForm(manifest);
426
- publishModal.style.display = 'flex';
427
- showPublishStep(1);
428
- })
429
- .catch(function (err) {
430
- pubError.textContent = err.message;
431
- pubError.style.display = 'block';
432
- publishModal.style.display = 'flex';
433
- showPublishStep(1);
434
- });
435
- });
436
-
437
- pubCancel.addEventListener('click', closePublishModal);
438
- publishModal.addEventListener('click', function (e) {
439
- if (e.target === publishModal) closePublishModal();
440
- });
441
-
442
- function closePublishModal() {
443
- publishModal.style.display = 'none';
444
- if (pubPollTimer) { clearInterval(pubPollTimer); pubPollTimer = null; }
445
- pubStep = 1;
446
- }
447
-
448
- pubBack.addEventListener('click', function () {
449
- if (pubStep > 1) showPublishStep(pubStep - 1);
450
- });
451
-
452
- pubNext.addEventListener('click', function () {
453
- pubError.style.display = 'none';
454
- if (pubStep === 1) {
455
- collectAndSaveManifest().then(function () { showPublishStep(2); startValidation(); });
456
- } else if (pubStep === 2 && pubValidationPassed) {
457
- showPublishStep(3);
458
- showCostInfo();
459
- } else if (pubStep === 3) {
460
- startBuildAndSubmit();
461
- } else if (pubStep === 4) {
462
- closePublishModal();
463
- }
464
- });
465
-
466
- function showPublishStep(step) {
467
- pubStep = step;
468
- for (var i = 1; i <= 4; i++) {
469
- var el = document.getElementById('publish-step-' + i);
470
- if (el) el.style.display = i === step ? 'block' : 'none';
471
- }
472
- var steps = document.querySelectorAll('.publish-step');
473
- steps.forEach(function (s, idx) {
474
- s.className = 'publish-step' + (idx + 1 === step ? ' active' : idx + 1 < step ? ' completed' : '');
475
- });
476
- pubBack.style.display = step > 1 && step < 4 ? 'inline-block' : 'none';
477
- if (step === 1) { pubNext.textContent = 'Validate'; pubNext.style.display = 'inline-block'; }
478
- else if (step === 2) { pubNext.textContent = 'Build & Submit'; pubNext.style.display = 'inline-block'; pubNext.disabled = !pubValidationPassed; }
479
- else if (step === 3) { pubNext.textContent = 'Confirm & Build'; pubNext.style.display = 'inline-block'; }
480
- else if (step === 4) { pubNext.textContent = 'Close'; pubNext.style.display = 'inline-block'; }
481
- }
482
-
483
- function populatePublishForm(m) {
484
- document.getElementById('pub-name').value = m.name || '';
485
- document.getElementById('pub-version').value = m.version || '1.0.0';
486
- document.getElementById('pub-description').value = m.description || '';
487
- document.getElementById('pub-expanded-desc').value = m.expandedDescription || '';
488
- document.getElementById('pub-archetype').value = m.archeType || 'tool';
489
- document.getElementById('pub-pricing-model').value = m.pricing?.model || 'free';
490
- document.getElementById('pub-price').value = m.pricing?.priceCents || '';
491
- document.getElementById('pub-price-row').style.display = m.pricing?.model === 'free' ? 'none' : 'block';
492
-
493
- // Tags
494
- var tagsList = document.getElementById('pub-tags-list');
495
- tagsList.innerHTML = '';
496
- (m.tags || []).forEach(function (t) { addPublishTag(t); });
497
-
498
- // Permissions
499
- var checkboxes = document.querySelectorAll('#pub-permissions input[type="checkbox"]');
500
- var perms = m.permissions || [];
501
- checkboxes.forEach(function (cb) { cb.checked = perms.indexOf(cb.value) !== -1; });
502
- }
503
-
504
- // Tag management
505
- document.getElementById('pub-tags-input').addEventListener('keydown', function (e) {
506
- if (e.key === 'Enter' || e.key === ',') {
507
- e.preventDefault();
508
- var val = this.value.trim().replace(/,/g, '');
509
- if (val) { addPublishTag(val); this.value = ''; }
510
- }
511
- });
512
-
513
- function addPublishTag(text) {
514
- var tag = document.createElement('span');
515
- tag.className = 'pub-tag';
516
- tag.innerHTML = escapeHtml(text) + ' <button class="pub-tag-remove">&times;</button>';
517
- tag.querySelector('.pub-tag-remove').addEventListener('click', function () { tag.remove(); });
518
- document.getElementById('pub-tags-list').appendChild(tag);
519
- }
520
-
521
- // Pricing model change
522
- document.getElementById('pub-pricing-model').addEventListener('change', function () {
523
- document.getElementById('pub-price-row').style.display = this.value === 'free' ? 'none' : 'block';
524
- });
525
-
526
- function collectManifest() {
527
- var tags = [];
528
- document.querySelectorAll('#pub-tags-list .pub-tag').forEach(function (t) {
529
- tags.push(t.textContent.replace('\u00d7', '').trim());
530
- });
531
- var perms = [];
532
- document.querySelectorAll('#pub-permissions input:checked').forEach(function (cb) { perms.push(cb.value); });
533
- var pricingModel = document.getElementById('pub-pricing-model').value;
534
- var pricing = { model: pricingModel, currency: 'usd' };
535
- if (pricingModel !== 'free') pricing.priceCents = parseInt(document.getElementById('pub-price').value, 10) || 0;
536
-
537
- return {
538
- name: document.getElementById('pub-name').value.trim(),
539
- version: document.getElementById('pub-version').value.trim(),
540
- description: document.getElementById('pub-description').value.trim(),
541
- expandedDescription: document.getElementById('pub-expanded-desc').value.trim() || undefined,
542
- main: (pubManifest && pubManifest.main) || 'src/index.tsx',
543
- archeType: document.getElementById('pub-archetype').value,
544
- tags: tags,
545
- pricing: pricing,
546
- permissions: perms,
547
- sdk: (pubManifest && pubManifest.sdk) || '^1.0.0',
548
- dependencies: pubManifest && pubManifest.dependencies,
549
- };
550
- }
551
-
552
- function collectAndSaveManifest() {
553
- var manifest = collectManifest();
554
- pubManifest = manifest;
555
- return fetch('/api/manifest', {
556
- method: 'PUT',
557
- headers: { 'Content-Type': 'application/json' },
558
- body: JSON.stringify(manifest),
559
- }).then(function (r) {
560
- if (!r.ok) throw new Error('Failed to save manifest');
561
- });
562
- }
563
-
564
- function startValidation() {
565
- pubValidationPassed = false;
566
- pubNext.disabled = true;
567
- document.getElementById('pub-validate-loading').style.display = 'flex';
568
- document.getElementById('pub-validate-results').style.display = 'none';
569
-
570
- fetch('/api/publish/validate', { method: 'POST' })
571
- .then(function (r) { return r.json(); })
572
- .then(function (data) {
573
- document.getElementById('pub-validate-loading').style.display = 'none';
574
- document.getElementById('pub-validate-results').style.display = 'block';
575
-
576
- var statusEl = document.getElementById('pub-validate-status');
577
- var errorsEl = document.getElementById('pub-validate-errors');
578
- var warningsEl = document.getElementById('pub-validate-warnings');
579
- errorsEl.innerHTML = '';
580
- warningsEl.innerHTML = '';
581
-
582
- if (data.valid && (!data.errors || data.errors.length === 0)) {
583
- statusEl.innerHTML = '<div class="pub-valid">Manifest is valid</div>';
584
- pubValidationPassed = true;
585
- pubNext.disabled = false;
586
- } else {
587
- statusEl.innerHTML = '<div style="color:#fca5a5;font-weight:500;margin-bottom:8px">Validation failed</div>';
588
- }
589
-
590
- if (data.errors) {
591
- data.errors.forEach(function (e) {
592
- var div = document.createElement('div');
593
- div.className = 'pub-error-item';
594
- div.textContent = e;
595
- errorsEl.appendChild(div);
596
- });
597
- }
598
- if (data.warnings) {
599
- data.warnings.forEach(function (w) {
600
- var div = document.createElement('div');
601
- div.className = 'pub-warn-item';
602
- div.textContent = w;
603
- warningsEl.appendChild(div);
604
- });
605
- }
606
-
607
- // Listing preview
608
- var preview = document.getElementById('pub-listing-preview');
609
- var m = pubManifest;
610
- preview.innerHTML =
611
- '<div class="pub-listing-name">' + escapeHtml(m.name) + '</div>' +
612
- '<div class="pub-listing-desc">' + escapeHtml(m.description) + '</div>' +
613
- '<div class="pub-listing-meta">' +
614
- '<span>v' + escapeHtml(m.version) + '</span>' +
615
- '<span>' + escapeHtml(m.archeType) + '</span>' +
616
- '<span>' + escapeHtml(m.pricing.model) + '</span>' +
617
- '</div>';
618
- })
619
- .catch(function (err) {
620
- document.getElementById('pub-validate-loading').style.display = 'none';
621
- pubError.textContent = err.message;
622
- pubError.style.display = 'block';
623
- });
624
- }
625
-
626
- function showCostInfo() {
627
- var costEl = document.getElementById('pub-cost-info');
628
- costEl.innerHTML = '<p>Submission cost: <strong>5,000 credits ($50.00)</strong> for first listing, <strong>100 credits ($1.00)</strong> for updates.</p>';
629
- fetch('/api/credits').then(function (r) { return r.json(); }).then(function (d) {
630
- if (d.balance != null && isFinite(d.balance)) {
631
- costEl.innerHTML += '<p style="margin-top:8px">Your balance: <strong>' + d.balance.toFixed(2) + ' credits</strong></p>';
632
- }
633
- }).catch(function () {});
634
- document.getElementById('pub-build-output').style.display = 'none';
635
- document.getElementById('pub-build-output').textContent = '';
636
- document.getElementById('pub-submit-progress').innerHTML = '';
637
- }
638
-
639
- function startBuildAndSubmit() {
640
- pubNext.style.display = 'none';
641
- pubBack.style.display = 'none';
642
- var outputEl = document.getElementById('pub-build-output');
643
- var progressEl = document.getElementById('pub-submit-progress');
644
- outputEl.style.display = 'block';
645
- outputEl.textContent = '';
646
- progressEl.innerHTML = '<div class="pub-phase active" id="phase-build">Building...</div>' +
647
- '<div class="pub-phase" id="phase-package">Packaging...</div>' +
648
- '<div class="pub-phase" id="phase-submit">Uploading & submitting...</div>';
649
-
650
- // SSE build
651
- fetch('/api/publish/build', { method: 'POST' })
652
- .then(function (response) {
653
- var reader = response.body.getReader();
654
- var decoder = new TextDecoder();
655
- var buffer = '';
656
-
657
- function pump() {
658
- return reader.read().then(function (chunk) {
659
- if (chunk.done) return;
660
- buffer += decoder.decode(chunk.value, { stream: true });
661
- var lines = buffer.split('\n');
662
- buffer = lines.pop() || '';
663
- for (var i = 0; i < lines.length; i++) {
664
- if (lines[i].indexOf('data: ') !== 0) continue;
665
- try {
666
- var parsed = JSON.parse(lines[i].slice(6));
667
- if (parsed.line) {
668
- outputEl.textContent += parsed.line + '\n';
669
- outputEl.scrollTop = outputEl.scrollHeight;
670
- }
671
- if (parsed.done) {
672
- if (parsed.code !== 0) throw new Error('Build failed (exit code ' + parsed.code + ')');
673
- document.getElementById('phase-build').className = 'pub-phase done';
674
- document.getElementById('phase-build').textContent = 'Build complete';
675
- return doPackageAndSubmit();
676
- }
677
- } catch (e) {
678
- if (e instanceof SyntaxError) continue;
679
- throw e;
680
- }
681
- }
682
- return pump();
683
- });
684
- }
685
- return pump();
686
- })
687
- .catch(function (err) {
688
- document.getElementById('phase-build').className = 'pub-phase error';
689
- document.getElementById('phase-build').textContent = 'Build failed: ' + err.message;
690
- pubBack.style.display = 'inline-block';
691
- });
692
- }
693
-
694
- function doPackageAndSubmit() {
695
- var progressPhase = document.getElementById('phase-package');
696
- progressPhase.className = 'pub-phase active';
697
-
698
- return fetch('/api/publish/package', { method: 'POST' })
699
- .then(function (r) {
700
- if (!r.ok) return r.json().then(function (e) { throw new Error(e.error); });
701
- return r.json();
702
- })
703
- .then(function (data) {
704
- progressPhase.className = 'pub-phase done';
705
- progressPhase.textContent = 'Packaged (' + Math.round(data.sizeBytes / 1024) + ' KB)';
706
- document.getElementById('phase-submit').className = 'pub-phase active';
707
-
708
- return fetch('/api/publish/submit', {
709
- method: 'POST',
710
- headers: { 'Content-Type': 'application/json' },
711
- body: JSON.stringify({}),
712
- });
713
- })
714
- .then(function (r) {
715
- if (!r.ok) return r.json().then(function (e) { throw new Error(e.error); });
716
- return r.json();
717
- })
718
- .then(function (data) {
719
- document.getElementById('phase-submit').className = 'pub-phase done';
720
- document.getElementById('phase-submit').textContent = 'Submitted for review';
721
- pubSubmissionId = data.submissionId;
722
- logMessage('info', 'Submitted: ' + pubSubmissionId);
723
- setTimeout(function () { showPublishStep(4); startStatusPolling(); }, 1000);
724
- })
725
- .catch(function (err) {
726
- var active = document.querySelector('.pub-phase.active');
727
- if (active) { active.className = 'pub-phase error'; active.textContent += ' — ' + err.message; }
728
- pubBack.style.display = 'inline-block';
729
- });
730
- }
731
-
732
- function startStatusPolling() {
733
- if (pubPollTimer) clearInterval(pubPollTimer);
734
- document.getElementById('pub-review-status').style.display = 'block';
735
- document.getElementById('pub-review-result').style.display = 'none';
736
-
737
- pubPollTimer = setInterval(function () {
738
- fetch('/api/publish/status/' + pubSubmissionId)
739
- .then(function (r) { return r.json(); })
740
- .then(function (data) {
741
- var sub = data.submission || data;
742
- var status = sub.status;
743
- var statusEl = document.getElementById('pub-review-status');
744
-
745
- if (status === 'reviewing') {
746
- statusEl.innerHTML = '<div class="status-spinner"></div><p>AI is reviewing your plugin...</p>';
747
- } else if (status === 'building') {
748
- statusEl.innerHTML = '<div class="status-spinner"></div><p>Building your plugin...</p>';
749
- } else if (status === 'published') {
750
- clearInterval(pubPollTimer);
751
- pubPollTimer = null;
752
- statusEl.style.display = 'none';
753
- document.getElementById('pub-review-result').style.display = 'block';
754
- document.getElementById('pub-review-result').innerHTML =
755
- '<div class="pub-congrats">Published!</div>' +
756
- '<p style="text-align:center;color:#a1a1aa;font-size:13px">Your plugin is now live on the Fias marketplace.</p>';
757
- logMessage('info', 'Plugin published!');
758
- } else if (status === 'rejected') {
759
- clearInterval(pubPollTimer);
760
- pubPollTimer = null;
761
- statusEl.style.display = 'none';
762
- document.getElementById('pub-review-result').style.display = 'block';
763
- var html = '<div class="pub-rejected">Submission Rejected</div>';
764
- if (sub.errorMessage) html += '<p style="color:#a1a1aa;font-size:12px;margin-bottom:8px">' + escapeHtml(sub.errorMessage) + '</p>';
765
- if (data.review) {
766
- html += '<p style="color:#6b7280;font-size:11px">Risk score: ' + (data.review.riskScore || 'N/A') + '</p>';
767
- }
768
- document.getElementById('pub-review-result').innerHTML = html;
769
- }
770
- })
771
- .catch(function () {});
772
- }, 3000);
773
- }
774
-
775
- // ────────────────────────────────────────────────────────────────
776
- // Iframe Init
777
- // ────────────────────────────────────────────────────────────────
778
-
779
- iframe.addEventListener('load', function () {
780
- if (!cachedConfig) return;
781
-
782
- sendToPlugin({
783
- type: 'init',
784
- messageId: 'init_0',
785
- payload: {
786
- archId: 'dev_harness',
787
- permissions: cachedConfig.permissions,
788
- theme: getTheme(),
789
- currentPath: '/',
790
- },
791
- });
792
- logMessage('send', 'init');
793
- });
794
-
795
- // ────────────────────────────────────────────────────────────────
796
- // Helpers
797
- // ────────────────────────────────────────────────────────────────
798
-
799
- function updateModeBadge() {
800
- var opposite = currentMode === 'live' ? 'Mock' : 'Live';
801
- modeBadge.textContent = currentMode.toUpperCase();
802
- modeBadge.className = 'mode-badge ' + (currentMode === 'live' ? 'mode-live' : 'mode-mock');
803
- modeToggle.textContent = 'Switch to ' + opposite;
804
- }
805
-
806
- function updateThemeBadge() {
807
- themeBadge.textContent = currentTheme.toUpperCase();
808
- themeBadge.className = 'theme-badge theme-' + currentTheme;
809
- document.body.style.background = currentTheme === 'light' ? '#ffffff' : '#0a0a0a';
810
-
811
- // Update toolbar to match theme
812
- var toolbar = document.querySelector('.toolbar');
813
- var logo = document.querySelector('.logo');
814
- var icons = document.querySelectorAll('.btn-icon');
815
- var env = document.getElementById('env-selector');
816
- if (currentTheme === 'light') {
817
- if (toolbar) { toolbar.style.background = '#f5f5f5'; toolbar.style.borderBottomColor = '#e5e5e5'; }
818
- if (logo) { logo.style.color = '#171717'; }
819
- if (env) { env.style.background = '#e5e5e5'; env.style.color = '#171717'; env.style.borderColor = '#d4d4d4'; }
820
- icons.forEach(function (btn) { btn.style.background = '#e5e5e5'; btn.style.color = '#171717'; btn.style.borderColor = '#d4d4d4'; });
821
- } else {
822
- if (toolbar) { toolbar.style.background = '#18181b'; toolbar.style.borderBottomColor = '#3f3f46'; }
823
- if (logo) { logo.style.color = '#e4e4e7'; }
824
- if (env) { env.style.background = '#27272a'; env.style.color = '#a1a1aa'; env.style.borderColor = '#3f3f46'; }
825
- icons.forEach(function (btn) { btn.style.background = '#27272a'; btn.style.color = '#e4e4e7'; btn.style.borderColor = '#3f3f46'; });
826
- }
827
- }
828
-
829
- function sendToPlugin(message) {
830
- iframe.contentWindow && iframe.contentWindow.postMessage(message, '*');
831
- }
832
-
833
- function checkRateLimit(type) {
834
- var limit = RATE_LIMITS[type];
835
- if (!limit) return;
836
-
837
- var now = Date.now();
838
- var bucket = rateBuckets[type];
839
-
840
- if (!bucket || now - bucket.windowStart > 60000) {
841
- rateBuckets[type] = { count: 1, windowStart: now };
842
- return;
843
- }
844
-
845
- if (bucket.count >= limit.maxPerMinute) {
846
- throw new Error(
847
- 'Rate limit exceeded for ' + type + ': max ' + limit.maxPerMinute + '/minute',
848
- );
849
- }
850
-
851
- bucket.count++;
852
- }
853
-
854
- var SYSTEM_FONTS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
855
- var MONO_FONTS = '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace';
856
- var SPACING = { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' };
857
- var FONTS = { body: SYSTEM_FONTS, heading: SYSTEM_FONTS, mono: MONO_FONTS };
858
-
859
- var COMPONENTS = {
860
- borderRadius: '0.5rem',
861
- buttonRadius: '0.375rem',
862
- cardRadius: '0.5rem',
863
- inputRadius: '0.375rem',
864
- shadowSm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
865
- shadowMd: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
866
- shadowLg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
867
- borderWidth: '1px',
868
- };
869
-
870
- function getTheme() {
871
- if (currentTheme === 'light') {
872
- return {
873
- mode: 'light',
874
- colors: {
875
- primary: '#171717', primaryText: '#ffffff',
876
- secondary: '#e5e5e5', accent: '#2563eb',
877
- background: '#ffffff', surface: '#fafafa',
878
- card: '#ffffff', cardText: '#0a0a0a',
879
- text: '#0a0a0a', textSecondary: '#737373',
880
- muted: '#f5f5f5', mutedText: '#a3a3a3',
881
- border: '#e5e5e5',
882
- error: '#dc2626', warning: '#d97706', success: '#16a34a', info: '#2563eb',
883
- },
884
- spacing: SPACING,
885
- fonts: FONTS,
886
- components: COMPONENTS,
887
- };
888
- }
889
- return {
890
- mode: 'dark',
891
- colors: {
892
- primary: '#ffffff', primaryText: '#0a0a0a',
893
- secondary: '#1f1f1f', accent: '#3b82f6',
894
- background: '#0a0a0a', surface: '#171717',
895
- card: '#141414', cardText: '#ffffff',
896
- text: '#ffffff', textSecondary: '#a6a6a6',
897
- muted: '#1e1e1e', mutedText: '#737373',
898
- border: '#2e2e2e',
899
- error: '#ef4444', warning: '#f59e0b', success: '#22c55e', info: '#3b82f6',
900
- },
901
- spacing: SPACING,
902
- fonts: FONTS,
903
- components: COMPONENTS,
904
- };
905
- }
906
-
907
- function fetchConfig() {
908
- return fetch('/api/config').then(function (r) {
909
- return r.json();
910
- });
911
- }
912
-
913
- function fetchCredits() {
914
- fetch('/api/credits')
915
- .then(function (r) {
916
- return r.json();
917
- })
918
- .then(function (data) {
919
- if (data.balance !== undefined && data.balance !== null && isFinite(data.balance)) {
920
- creditBalance.textContent = 'Credits: ' + data.balance.toFixed(2);
921
- }
922
- })
923
- .catch(function () {});
924
- }
925
-
926
- function logMessage(direction, type, payload) {
927
- messageCount++;
928
- consoleCount.textContent = messageCount + ' messages';
929
-
930
- var entry = document.createElement('div');
931
- entry.className = 'log-entry';
932
-
933
- var time = new Date().toLocaleTimeString();
934
- var cls = 'log-info';
935
- if (direction === 'error') cls = 'log-error';
936
- if (direction === 'warn' || direction === 'cost') cls = 'log-warn';
937
- if (direction === 'toast') cls = 'log-warn';
938
-
939
- var text = '<span class="log-time">' + escapeHtml(time) + '</span>';
940
- text +=
941
- '<span class="' +
942
- cls +
943
- '">[' +
944
- escapeHtml(direction.toUpperCase()) +
945
- '] ' +
946
- escapeHtml(String(type)) +
947
- '</span>';
948
- if (payload && typeof payload === 'object') {
949
- text +=
950
- ' <span style="color:#6b7280">' +
951
- escapeHtml(JSON.stringify(payload).substring(0, 120)) +
952
- '</span>';
953
- }
954
-
955
- entry.innerHTML = text;
956
- consoleBody.appendChild(entry);
957
- consoleBody.scrollTop = consoleBody.scrollHeight;
958
- }
959
-
960
- function escapeHtml(str) {
961
- var div = document.createElement('div');
962
- div.appendChild(document.createTextNode(str));
963
- return div.innerHTML;
964
- }
965
-
966
- function checkPluginReachable(url, callback) {
967
- var done = false;
968
-
969
- fetch('/api/check-plugin')
970
- .then(function (r) { return r.json(); })
971
- .then(function (data) {
972
- if (!done) {
973
- done = true;
974
- callback(data.reachable);
975
- }
976
- })
977
- .catch(function () {
978
- if (!done) {
979
- done = true;
980
- callback(true);
981
- }
982
- });
983
-
984
- setTimeout(function () {
985
- if (!done) {
986
- done = true;
987
- callback(false);
988
- }
989
- }, 5000);
990
- }
991
-
992
- function showPluginError(url) {
993
- pluginStatus.classList.add('error');
994
- pluginStatus.innerHTML =
995
- '<p><strong>Plugin server not reachable</strong></p>' +
996
- '<p>The harness cannot connect to your plugin at<br/><code>' + escapeHtml(url) + '</code></p>' +
997
- '<p>Make sure your plugin dev server is running:</p>' +
998
- '<p><code>npm run dev</code></p>' +
999
- '<p style="color:#6b7280;font-size:12px;margin-top:8px;">' +
1000
- 'The harness will automatically retry when you click the reload button (&#8635;).</p>';
1001
- iframe.classList.add('hidden');
1002
- }
1003
- })();