@fias/plugin-dev-harness 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/config-loader.test.js.map +1 -1
- package/dist/mocks/entity-responses.test.js.map +1 -1
- package/dist/mocks/theme.d.ts +21 -5
- package/dist/mocks/theme.d.ts.map +1 -1
- package/dist/mocks/theme.js +24 -9
- package/dist/mocks/theme.js.map +1 -1
- package/dist/mocks/theme.test.js +25 -9
- package/dist/mocks/theme.test.js.map +1 -1
- package/dist/server/harness-server.d.ts.map +1 -1
- package/dist/server/harness-server.js +278 -210
- package/dist/server/harness-server.js.map +1 -1
- package/dist/server/harness-server.test.js +5 -6
- package/dist/server/harness-server.test.js.map +1 -1
- package/dist/server/static/harness.css +181 -9
- package/dist/server/static/harness.html +24 -5
- package/dist/server/static/harness.js +238 -28
- package/package.json +1 -1
|
@@ -49,27 +49,71 @@ const fs = __importStar(require("fs"));
|
|
|
49
49
|
const chalk_1 = __importDefault(require("chalk"));
|
|
50
50
|
const mock_handler_1 = require("../bridge/mock-handler");
|
|
51
51
|
const live_handler_1 = require("../bridge/live-handler");
|
|
52
|
+
const credentials_1 = require("../config/credentials");
|
|
52
53
|
async function startHarnessServer(options) {
|
|
53
54
|
const app = (0, express_1.default)();
|
|
54
55
|
app.use(express_1.default.json());
|
|
55
|
-
//
|
|
56
|
-
|
|
56
|
+
// Mutable mode — can be toggled at runtime via UI
|
|
57
|
+
let currentMode = options.isLive ? 'live' : 'mock';
|
|
58
|
+
let currentApiKey = options.apiKey;
|
|
59
|
+
// Always create mock handler
|
|
60
|
+
const mockHandler = new mock_handler_1.MockBridgeHandler({
|
|
61
|
+
mockUser: options.mockUser,
|
|
62
|
+
mockTheme: options.mockTheme,
|
|
63
|
+
mockEntities: options.mockEntities,
|
|
64
|
+
});
|
|
65
|
+
// Create live handler (recreated when credentials change)
|
|
66
|
+
let liveHandler = currentApiKey
|
|
57
67
|
? new live_handler_1.LiveBridgeHandler({
|
|
58
|
-
apiKey:
|
|
68
|
+
apiKey: currentApiKey,
|
|
59
69
|
apiUrl: options.apiUrl,
|
|
60
70
|
permissions: options.permissions,
|
|
61
71
|
mockUser: options.mockUser,
|
|
62
72
|
mockTheme: options.mockTheme,
|
|
63
73
|
})
|
|
64
|
-
:
|
|
74
|
+
: null;
|
|
75
|
+
function getActiveHandler() {
|
|
76
|
+
return currentMode === 'live' && liveHandler ? liveHandler : mockHandler;
|
|
77
|
+
}
|
|
78
|
+
// Mode toggle endpoint — called by harness.js
|
|
79
|
+
app.post('/api/mode', (req, res) => {
|
|
80
|
+
const { mode } = req.body;
|
|
81
|
+
if (mode === 'live') {
|
|
82
|
+
if (!liveHandler) {
|
|
83
|
+
res.status(400).json({ error: 'No API key configured. Use the login button first.' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
currentMode = 'live';
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
currentMode = 'mock';
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk_1.default.dim(` Mode switched to ${currentMode.toUpperCase()}`));
|
|
92
|
+
res.json({ mode: currentMode });
|
|
93
|
+
});
|
|
94
|
+
// Login endpoint — saves API key and creates live handler
|
|
95
|
+
app.post('/api/login', (req, res) => {
|
|
96
|
+
const { apiKey } = req.body;
|
|
97
|
+
if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) {
|
|
98
|
+
res.status(400).json({ error: 'API key is required' });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
currentApiKey = apiKey.trim();
|
|
102
|
+
(0, credentials_1.saveCredentials)({ apiKey: currentApiKey });
|
|
103
|
+
liveHandler = new live_handler_1.LiveBridgeHandler({
|
|
104
|
+
apiKey: currentApiKey,
|
|
105
|
+
apiUrl: options.apiUrl,
|
|
106
|
+
permissions: options.permissions,
|
|
65
107
|
mockUser: options.mockUser,
|
|
66
108
|
mockTheme: options.mockTheme,
|
|
67
|
-
mockEntities: options.mockEntities,
|
|
68
109
|
});
|
|
110
|
+
console.log(chalk_1.default.green(' API key saved to ~/.fias/credentials'));
|
|
111
|
+
res.json({ success: true });
|
|
112
|
+
});
|
|
69
113
|
// Bridge API endpoint — called by harness.js
|
|
70
114
|
app.post('/api/bridge', async (req, res) => {
|
|
71
115
|
try {
|
|
72
|
-
const result = await
|
|
116
|
+
const result = await getActiveHandler().handle(req.body);
|
|
73
117
|
res.json(result);
|
|
74
118
|
}
|
|
75
119
|
catch (err) {
|
|
@@ -78,15 +122,15 @@ async function startHarnessServer(options) {
|
|
|
78
122
|
});
|
|
79
123
|
}
|
|
80
124
|
});
|
|
81
|
-
// Credit balance endpoint
|
|
125
|
+
// Credit balance endpoint
|
|
82
126
|
app.get('/api/credits', async (_req, res) => {
|
|
83
|
-
if (!
|
|
84
|
-
res.json({ balance:
|
|
127
|
+
if (currentMode !== 'live' || !currentApiKey) {
|
|
128
|
+
res.json({ balance: null });
|
|
85
129
|
return;
|
|
86
130
|
}
|
|
87
131
|
try {
|
|
88
132
|
const response = await fetch(`${options.apiUrl}/developer/credits`, {
|
|
89
|
-
headers: { Authorization: `Bearer ${
|
|
133
|
+
headers: { Authorization: `Bearer ${currentApiKey}` },
|
|
90
134
|
});
|
|
91
135
|
if (!response.ok) {
|
|
92
136
|
res.status(response.status).json({ error: 'Failed to fetch credits' });
|
|
@@ -100,11 +144,25 @@ async function startHarnessServer(options) {
|
|
|
100
144
|
.json({ error: err instanceof Error ? err.message : 'Failed to fetch credits' });
|
|
101
145
|
}
|
|
102
146
|
});
|
|
147
|
+
// Plugin reachability check — called by harness.js before loading iframe
|
|
148
|
+
app.get('/api/check-plugin', async (_req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
152
|
+
const response = await fetch(options.pluginUrl, { signal: controller.signal });
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
res.json({ reachable: response.ok });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
res.json({ reachable: false });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
103
160
|
// Harness config endpoint — provides config to the frontend
|
|
104
161
|
app.get('/api/config', (_req, res) => {
|
|
105
162
|
res.json({
|
|
106
163
|
pluginUrl: options.pluginUrl,
|
|
107
|
-
|
|
164
|
+
mode: currentMode,
|
|
165
|
+
hasCredentials: Boolean(currentApiKey),
|
|
108
166
|
permissions: options.permissions,
|
|
109
167
|
mockTheme: options.mockTheme,
|
|
110
168
|
});
|
|
@@ -137,8 +195,20 @@ async function startHarnessServer(options) {
|
|
|
137
195
|
});
|
|
138
196
|
return new Promise((resolve) => {
|
|
139
197
|
app.listen(options.port, () => {
|
|
140
|
-
console.log(chalk_1.default.green(
|
|
141
|
-
console.log(chalk_1.default.dim(
|
|
198
|
+
console.log(chalk_1.default.green(`\n Harness server running at http://localhost:${options.port}`));
|
|
199
|
+
console.log(chalk_1.default.dim(` Plugin URL: ${options.pluginUrl}`));
|
|
200
|
+
console.log(chalk_1.default.dim(` Mode: ${currentMode.toUpperCase()}${currentApiKey ? '' : ' (no API key — click MOCK badge in toolbar to login)'}\n`));
|
|
201
|
+
// Check if plugin dev server is reachable
|
|
202
|
+
checkPluginReachable(options.pluginUrl).then((reachable) => {
|
|
203
|
+
if (reachable) {
|
|
204
|
+
console.log(chalk_1.default.green(` ✓ Plugin server is reachable at ${options.pluginUrl}\n`));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
console.log(chalk_1.default.yellow(` ⚠ Plugin server not reachable at ${options.pluginUrl}\n` +
|
|
208
|
+
` Make sure your plugin dev server is running (npm run dev)\n`));
|
|
209
|
+
}
|
|
210
|
+
console.log(chalk_1.default.dim(' Press Ctrl+C to stop\n'));
|
|
211
|
+
});
|
|
142
212
|
resolve();
|
|
143
213
|
});
|
|
144
214
|
});
|
|
@@ -146,37 +216,55 @@ async function startHarnessServer(options) {
|
|
|
146
216
|
/**
|
|
147
217
|
* Fallback HTML when static files haven't been copied to dist.
|
|
148
218
|
* This enables the harness to work during development.
|
|
219
|
+
* Mirrors the structure of harness.html but with inline styles/scripts.
|
|
149
220
|
*/
|
|
150
221
|
function generateFallbackHtml(options) {
|
|
222
|
+
const initialMode = options.isLive ? 'live' : 'mock';
|
|
151
223
|
return `<!DOCTYPE html>
|
|
152
224
|
<html lang="en">
|
|
153
225
|
<head>
|
|
154
226
|
<meta charset="UTF-8" />
|
|
155
227
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
156
|
-
<title>
|
|
228
|
+
<title>Fias Arche Dev</title>
|
|
157
229
|
<style>${EMBEDDED_CSS}</style>
|
|
158
230
|
</head>
|
|
159
231
|
<body>
|
|
160
232
|
<div class="toolbar">
|
|
161
233
|
<div class="toolbar-left">
|
|
162
|
-
<span class="logo">
|
|
163
|
-
<
|
|
164
|
-
${
|
|
165
|
-
</
|
|
166
|
-
<
|
|
167
|
-
${options.permissions.map((p) => `<span class="perm-badge">${p}</span>`).join('')}
|
|
168
|
-
</div>
|
|
234
|
+
<span class="logo">Fias Arche Dev</span>
|
|
235
|
+
<button id="mode-badge" class="mode-badge mode-${initialMode === 'live' ? 'live' : 'mock'}" title="Click to toggle mode">
|
|
236
|
+
${initialMode.toUpperCase()}
|
|
237
|
+
</button>
|
|
238
|
+
<span id="plugin-url" class="plugin-url">${options.pluginUrl}</span>
|
|
169
239
|
</div>
|
|
170
240
|
<div class="toolbar-right">
|
|
171
|
-
<span id="credit-balance" class="credit-balance" style="display:
|
|
241
|
+
<span id="credit-balance" class="credit-balance" style="display: none"></span>
|
|
242
|
+
<span id="theme-badge" class="theme-badge theme-${options.mockTheme || 'dark'}">${(options.mockTheme || 'dark').toUpperCase()}</span>
|
|
172
243
|
<button id="theme-toggle" class="btn-icon" title="Toggle theme">☀/☾</button>
|
|
173
244
|
<button id="reload-btn" class="btn-icon" title="Reload plugin">↻</button>
|
|
174
245
|
</div>
|
|
175
246
|
</div>
|
|
247
|
+
<div id="login-modal" class="modal-overlay" style="display: none">
|
|
248
|
+
<div class="modal">
|
|
249
|
+
<h3>Connect to FIAS</h3>
|
|
250
|
+
<p>Enter your API key to enable live mode with real AI responses.</p>
|
|
251
|
+
<p class="modal-hint">Get an API key from your FIAS platform account settings.</p>
|
|
252
|
+
<input id="login-input" type="password" class="modal-input" placeholder="fias_sk_..." autocomplete="off" />
|
|
253
|
+
<div id="login-error" class="modal-error" style="display: none"></div>
|
|
254
|
+
<div class="modal-actions">
|
|
255
|
+
<button id="login-cancel" class="btn-secondary">Cancel</button>
|
|
256
|
+
<button id="login-submit" class="btn-primary">Save & Connect</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div id="plugin-status" class="plugin-status">
|
|
261
|
+
<div class="status-spinner"></div>
|
|
262
|
+
<p>Connecting to plugin server...</p>
|
|
263
|
+
</div>
|
|
176
264
|
<iframe
|
|
177
265
|
id="plugin-iframe"
|
|
178
|
-
|
|
179
|
-
sandbox="allow-scripts allow-forms"
|
|
266
|
+
class="hidden"
|
|
267
|
+
sandbox="allow-scripts allow-forms allow-same-origin"
|
|
180
268
|
></iframe>
|
|
181
269
|
<div id="console-panel" class="console-panel">
|
|
182
270
|
<div class="console-header" id="console-toggle">
|
|
@@ -189,21 +277,58 @@ function generateFallbackHtml(options) {
|
|
|
189
277
|
</body>
|
|
190
278
|
</html>`;
|
|
191
279
|
}
|
|
280
|
+
async function checkPluginReachable(url) {
|
|
281
|
+
try {
|
|
282
|
+
const controller = new AbortController();
|
|
283
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
284
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
return response.ok;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
192
292
|
const EMBEDDED_CSS = `
|
|
193
293
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
194
294
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0f; color: #e4e4e7; display: flex; flex-direction: column; height: 100vh; }
|
|
195
295
|
.toolbar { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: #18181b; border-bottom: 1px solid #3f3f46; flex-shrink: 0; }
|
|
196
296
|
.toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 12px; }
|
|
197
297
|
.logo { font-weight: 600; font-size: 14px; color: #a78bfa; }
|
|
198
|
-
.mode-badge { padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
|
|
298
|
+
.mode-badge { padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; border: 1px solid transparent; cursor: pointer; transition: opacity 0.15s; }
|
|
299
|
+
.mode-badge:hover { opacity: 0.8; }
|
|
199
300
|
.mode-mock { background: #166534; color: #86efac; }
|
|
200
301
|
.mode-live { background: #854d0e; color: #fde047; }
|
|
201
|
-
.
|
|
202
|
-
.
|
|
302
|
+
.theme-badge { padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
|
|
303
|
+
.theme-dark { background: #1e1b4b; color: #a78bfa; }
|
|
304
|
+
.theme-light { background: #ede9fe; color: #6d28d9; }
|
|
203
305
|
.credit-balance { font-size: 12px; color: #fde047; }
|
|
204
306
|
.btn-icon { background: #27272a; border: 1px solid #3f3f46; color: #e4e4e7; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 14px; }
|
|
205
307
|
.btn-icon:hover { background: #3f3f46; }
|
|
308
|
+
.plugin-url { font-size: 11px; color: #6b7280; font-family: monospace; }
|
|
309
|
+
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
|
310
|
+
.modal { background: #27272a; border: 1px solid #3f3f46; border-radius: 8px; padding: 24px; width: 420px; max-width: 90vw; }
|
|
311
|
+
.modal h3 { font-size: 16px; color: #e4e4e7; margin-bottom: 8px; }
|
|
312
|
+
.modal p { font-size: 13px; color: #a1a1aa; line-height: 1.5; margin-bottom: 4px; }
|
|
313
|
+
.modal-hint { font-size: 12px !important; color: #6b7280 !important; margin-bottom: 16px !important; }
|
|
314
|
+
.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; }
|
|
315
|
+
.modal-input:focus { border-color: #a78bfa; }
|
|
316
|
+
.modal-error { font-size: 12px; color: #fca5a5; margin-bottom: 12px; }
|
|
317
|
+
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
318
|
+
.btn-primary { background: #6d28d9; color: #e4e4e7; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
319
|
+
.btn-primary:hover { background: #7c3aed; }
|
|
320
|
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
321
|
+
.btn-secondary { background: transparent; color: #a1a1aa; border: 1px solid #3f3f46; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
|
322
|
+
.btn-secondary:hover { background: #3f3f46; }
|
|
323
|
+
.plugin-status { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; color: #a1a1aa; font-size: 14px; }
|
|
324
|
+
.plugin-status.hidden { display: none; }
|
|
325
|
+
.plugin-status.error { color: #fca5a5; }
|
|
326
|
+
.plugin-status p { max-width: 500px; text-align: center; line-height: 1.6; }
|
|
327
|
+
.plugin-status code { background: #27272a; padding: 2px 8px; border-radius: 4px; font-family: monospace; font-size: 13px; color: #e4e4e7; }
|
|
328
|
+
.status-spinner { width: 24px; height: 24px; border: 3px solid #3f3f46; border-top-color: #a78bfa; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
329
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
206
330
|
#plugin-iframe { flex: 1; border: none; width: 100%; }
|
|
331
|
+
#plugin-iframe.hidden { display: none; }
|
|
207
332
|
.console-panel { flex-shrink: 0; border-top: 1px solid #3f3f46; background: #18181b; max-height: 250px; display: flex; flex-direction: column; }
|
|
208
333
|
.console-header { display: flex; justify-content: space-between; padding: 6px 16px; cursor: pointer; font-size: 12px; color: #a1a1aa; border-bottom: 1px solid #27272a; }
|
|
209
334
|
.console-body { overflow-y: auto; padding: 8px 16px; font-family: monospace; font-size: 11px; flex: 1; display: none; }
|
|
@@ -215,218 +340,161 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|
|
215
340
|
.log-time { color: #6b7280; margin-right: 8px; }
|
|
216
341
|
.log-cost { color: #fbbf24; margin-left: 8px; }
|
|
217
342
|
`;
|
|
343
|
+
// The embedded JS mirrors harness.js but is served inline for the fallback page.
|
|
344
|
+
// It uses the same API endpoints as the static version.
|
|
218
345
|
const EMBEDDED_JS = `
|
|
219
346
|
(function() {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
347
|
+
var iframe = document.getElementById('plugin-iframe');
|
|
348
|
+
var consoleBody = document.getElementById('console-body');
|
|
349
|
+
var consoleCount = document.getElementById('console-count');
|
|
350
|
+
var consoleToggle = document.getElementById('console-toggle');
|
|
351
|
+
var creditBalance = document.getElementById('credit-balance');
|
|
352
|
+
var themeToggle = document.getElementById('theme-toggle');
|
|
353
|
+
var reloadBtn = document.getElementById('reload-btn');
|
|
354
|
+
var modeBadge = document.getElementById('mode-badge');
|
|
355
|
+
var themeBadge = document.getElementById('theme-badge');
|
|
356
|
+
var pluginStatus = document.getElementById('plugin-status');
|
|
357
|
+
var loginModal = document.getElementById('login-modal');
|
|
358
|
+
var loginInput = document.getElementById('login-input');
|
|
359
|
+
var loginError = document.getElementById('login-error');
|
|
360
|
+
var loginSubmit = document.getElementById('login-submit');
|
|
361
|
+
var loginCancel = document.getElementById('login-cancel');
|
|
227
362
|
|
|
228
|
-
|
|
229
|
-
|
|
363
|
+
var messageCount = 0;
|
|
364
|
+
var currentTheme = 'dark';
|
|
365
|
+
var currentMode = 'mock';
|
|
366
|
+
var hasCredentials = false;
|
|
367
|
+
var cachedConfig = null;
|
|
230
368
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
369
|
+
var PERMISSION_MAP = {
|
|
370
|
+
get_user: 'user:profile:read', get_theme: 'theme:read', entity_invoke: 'entities:invoke',
|
|
371
|
+
storage_read: 'storage:sandbox', storage_write: 'storage:sandbox',
|
|
372
|
+
storage_list: 'storage:sandbox', storage_delete: 'storage:sandbox',
|
|
373
|
+
};
|
|
374
|
+
var RATE_LIMITS = {
|
|
375
|
+
entity_invoke: { maxPerMinute: 60 }, storage_write: { maxPerMinute: 120 },
|
|
376
|
+
storage_read: { maxPerMinute: 300 }, storage_list: { maxPerMinute: 60 },
|
|
377
|
+
storage_delete: { maxPerMinute: 60 },
|
|
378
|
+
};
|
|
379
|
+
var rateBuckets = {};
|
|
238
380
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
381
|
+
var SYSTEM_FONTS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
382
|
+
var MONO_FONTS = '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace';
|
|
383
|
+
var SPACING = { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' };
|
|
384
|
+
var FONTS = { body: SYSTEM_FONTS, heading: SYSTEM_FONTS, mono: MONO_FONTS };
|
|
243
385
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
386
|
+
fetch('/api/config').then(function(r) { return r.json(); }).then(function(config) {
|
|
387
|
+
cachedConfig = config;
|
|
388
|
+
currentTheme = config.mockTheme || 'dark';
|
|
389
|
+
currentMode = config.mode || 'mock';
|
|
390
|
+
hasCredentials = config.hasCredentials || false;
|
|
391
|
+
updateThemeBadge();
|
|
392
|
+
updateModeBadge();
|
|
393
|
+
if (currentMode === 'live') { creditBalance.style.display = 'inline'; fetchCredits(); }
|
|
394
|
+
checkPluginReachable(function(reachable) {
|
|
395
|
+
if (reachable) { pluginStatus.classList.add('hidden'); iframe.classList.remove('hidden'); iframe.src = config.pluginUrl; }
|
|
396
|
+
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'); }
|
|
397
|
+
});
|
|
247
398
|
});
|
|
248
399
|
|
|
249
|
-
|
|
250
|
-
|
|
400
|
+
consoleToggle.addEventListener('click', function() { consoleBody.classList.toggle('open'); });
|
|
401
|
+
reloadBtn.addEventListener('click', function() { if (cachedConfig) { iframe.src = cachedConfig.pluginUrl; } });
|
|
402
|
+
|
|
403
|
+
themeToggle.addEventListener('click', function() {
|
|
251
404
|
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
405
|
+
updateThemeBadge();
|
|
252
406
|
sendToPlugin({ type: 'theme_update', messageId: 'theme_' + Date.now(), payload: getTheme() });
|
|
253
407
|
});
|
|
254
408
|
|
|
255
|
-
function
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
409
|
+
modeBadge.addEventListener('click', function() {
|
|
410
|
+
if (currentMode === 'mock') {
|
|
411
|
+
if (!hasCredentials) { showLoginModal(); return; }
|
|
412
|
+
switchMode('live');
|
|
413
|
+
} else { switchMode('mock'); }
|
|
414
|
+
});
|
|
261
415
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
};
|
|
416
|
+
function switchMode(newMode) {
|
|
417
|
+
fetch('/api/mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: newMode }) })
|
|
418
|
+
.then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.error); }); return r.json(); })
|
|
419
|
+
.then(function(data) {
|
|
420
|
+
currentMode = data.mode; updateModeBadge();
|
|
421
|
+
if (currentMode === 'live') { creditBalance.style.display = 'inline'; fetchCredits(); }
|
|
422
|
+
else { creditBalance.style.display = 'none'; creditBalance.textContent = ''; }
|
|
423
|
+
}).catch(function(err) { logMessage('error', err.message); });
|
|
424
|
+
}
|
|
272
425
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
storage_delete: { maxPerMinute: 60 },
|
|
280
|
-
};
|
|
281
|
-
const rateBuckets = {};
|
|
426
|
+
function showLoginModal() { loginModal.style.display = 'flex'; loginInput.value = ''; loginError.style.display = 'none'; loginInput.focus(); }
|
|
427
|
+
function hideLoginModal() { loginModal.style.display = 'none'; }
|
|
428
|
+
loginCancel.addEventListener('click', hideLoginModal);
|
|
429
|
+
loginModal.addEventListener('click', function(e) { if (e.target === loginModal) hideLoginModal(); });
|
|
430
|
+
loginInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') submitLogin(); if (e.key === 'Escape') hideLoginModal(); });
|
|
431
|
+
loginSubmit.addEventListener('click', submitLogin);
|
|
282
432
|
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
if (!
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (bucket.count >= limit.maxPerMinute) {
|
|
293
|
-
throw new Error('Rate limit exceeded for ' + type + ': max ' + limit.maxPerMinute + '/minute');
|
|
294
|
-
}
|
|
295
|
-
bucket.count++;
|
|
433
|
+
function submitLogin() {
|
|
434
|
+
var apiKey = loginInput.value.trim();
|
|
435
|
+
if (!apiKey) { loginError.textContent = 'Please enter an API key.'; loginError.style.display = 'block'; return; }
|
|
436
|
+
loginSubmit.disabled = true; loginSubmit.textContent = 'Connecting...';
|
|
437
|
+
fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: apiKey }) })
|
|
438
|
+
.then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.error); }); return r.json(); })
|
|
439
|
+
.then(function() { hasCredentials = true; hideLoginModal(); switchMode('live'); })
|
|
440
|
+
.catch(function(err) { loginError.textContent = err.message; loginError.style.display = 'block'; })
|
|
441
|
+
.finally(function() { loginSubmit.disabled = false; loginSubmit.textContent = 'Save & Connect'; });
|
|
296
442
|
}
|
|
297
443
|
|
|
298
|
-
|
|
299
|
-
window.addEventListener('message', async (event) => {
|
|
444
|
+
window.addEventListener('message', function(event) {
|
|
300
445
|
if (event.source !== iframe.contentWindow) return;
|
|
301
|
-
|
|
302
|
-
const data = event.data;
|
|
446
|
+
var data = event.data;
|
|
303
447
|
if (!data || typeof data !== 'object' || !data.type || !data.messageId) return;
|
|
304
|
-
|
|
305
448
|
logMessage('recv', data.type, data.payload);
|
|
306
|
-
|
|
307
|
-
// Fire-and-forget messages
|
|
308
449
|
if (data.type === 'ready') return;
|
|
450
|
+
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; }
|
|
451
|
+
if (data.type === 'toast') { var msg = data.payload && data.payload.message; if (typeof msg === 'string') logMessage('toast', msg); return; }
|
|
452
|
+
if (data.type === 'navigate') { var p = data.payload && data.payload.path; if (typeof p === 'string') logMessage('nav', p); return; }
|
|
453
|
+
handleRequest(data);
|
|
454
|
+
});
|
|
309
455
|
|
|
310
|
-
|
|
311
|
-
const height = data.payload && data.payload.height;
|
|
312
|
-
if (typeof height === 'number' && height > 0) {
|
|
313
|
-
iframe.style.height = height + 'px';
|
|
314
|
-
iframe.style.flex = 'none';
|
|
315
|
-
}
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (data.type === 'toast') {
|
|
320
|
-
const msg = data.payload && data.payload.message;
|
|
321
|
-
if (typeof msg === 'string') {
|
|
322
|
-
logMessage('toast', msg, data.payload);
|
|
323
|
-
}
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (data.type === 'navigate') {
|
|
328
|
-
const navPath = data.payload && data.payload.path;
|
|
329
|
-
if (typeof navPath === 'string') {
|
|
330
|
-
logMessage('nav', navPath);
|
|
331
|
-
}
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Request/response messages
|
|
456
|
+
function handleRequest(data) {
|
|
336
457
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (requiredPerm) {
|
|
340
|
-
const config = await fetch('/api/config').then(r => r.json());
|
|
341
|
-
if (!config.permissions.includes(requiredPerm)) {
|
|
342
|
-
throw new Error('Permission denied: ' + requiredPerm + ' not granted');
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
458
|
+
var requiredPerm = PERMISSION_MAP[data.type];
|
|
459
|
+
if (requiredPerm && cachedConfig && cachedConfig.permissions.indexOf(requiredPerm) === -1) throw new Error('Permission denied: ' + requiredPerm);
|
|
346
460
|
checkRateLimit(data.type);
|
|
461
|
+
} catch (err) { logMessage('error', err.message); sendToPlugin({ type: 'response', messageId: data.messageId, payload: null, error: err.message }); return; }
|
|
462
|
+
fetch('/api/bridge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: data.type, payload: data.payload }) })
|
|
463
|
+
.then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || 'Bridge call failed'); }); return r.json(); })
|
|
464
|
+
.then(function(result) {
|
|
465
|
+
logMessage('send', 'response', result);
|
|
466
|
+
if (data.type === 'entity_invoke' && result.metadata && result.metadata.cost > 0) { logMessage('cost', 'Credits: ' + result.metadata.cost.toFixed(4)); fetchCredits(); }
|
|
467
|
+
sendToPlugin({ type: 'response', messageId: data.messageId, payload: result });
|
|
468
|
+
})
|
|
469
|
+
.catch(function(err) { logMessage('error', err.message); sendToPlugin({ type: 'response', messageId: data.messageId, payload: null, error: err.message }); });
|
|
470
|
+
}
|
|
347
471
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (!response.ok) {
|
|
355
|
-
const err = await response.json().catch(() => ({}));
|
|
356
|
-
throw new Error(err.error || 'Bridge call failed');
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const result = await response.json();
|
|
360
|
-
logMessage('send', 'response', result);
|
|
361
|
-
|
|
362
|
-
// Show cost for entity invocations
|
|
363
|
-
if (data.type === 'entity_invoke' && result.metadata && result.metadata.cost > 0) {
|
|
364
|
-
logMessage('cost', 'Credits used: ' + result.metadata.cost.toFixed(4));
|
|
365
|
-
fetchCredits();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
sendToPlugin({ type: 'response', messageId: data.messageId, payload: result });
|
|
369
|
-
} catch (err) {
|
|
370
|
-
logMessage('error', err.message);
|
|
371
|
-
sendToPlugin({ type: 'response', messageId: data.messageId, payload: null, error: err.message });
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// Send init message when iframe loads
|
|
376
|
-
iframe.addEventListener('load', () => {
|
|
377
|
-
fetch('/api/config')
|
|
378
|
-
.then(r => r.json())
|
|
379
|
-
.then(config => {
|
|
380
|
-
sendToPlugin({
|
|
381
|
-
type: 'init',
|
|
382
|
-
messageId: 'init_0',
|
|
383
|
-
payload: {
|
|
384
|
-
archId: 'dev_harness',
|
|
385
|
-
permissions: config.permissions,
|
|
386
|
-
theme: getTheme(),
|
|
387
|
-
currentPath: '/',
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
logMessage('send', 'init');
|
|
391
|
-
});
|
|
472
|
+
iframe.addEventListener('load', function() {
|
|
473
|
+
if (!cachedConfig) return;
|
|
474
|
+
sendToPlugin({ type: 'init', messageId: 'init_0', payload: { archId: 'dev_harness', permissions: cachedConfig.permissions, theme: getTheme(), currentPath: '/' } });
|
|
475
|
+
logMessage('send', 'init');
|
|
392
476
|
});
|
|
393
477
|
|
|
394
|
-
function
|
|
395
|
-
|
|
478
|
+
function updateModeBadge() {
|
|
479
|
+
modeBadge.textContent = currentMode.toUpperCase();
|
|
480
|
+
modeBadge.className = 'mode-badge ' + (currentMode === 'live' ? 'mode-live' : 'mode-mock');
|
|
396
481
|
}
|
|
397
|
-
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
creditBalance.textContent = 'Credits: ' + data.balance.toFixed(2);
|
|
404
|
-
}
|
|
405
|
-
})
|
|
406
|
-
.catch(() => {});
|
|
482
|
+
function updateThemeBadge() { themeBadge.textContent = currentTheme.toUpperCase(); themeBadge.className = 'theme-badge theme-' + currentTheme; }
|
|
483
|
+
function sendToPlugin(message) { iframe.contentWindow && iframe.contentWindow.postMessage(message, '*'); }
|
|
484
|
+
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++; }
|
|
485
|
+
function getTheme() {
|
|
486
|
+
if (currentTheme === 'light') return { mode: 'light', colors: { primary: '#7c3aed', secondary: '#ede9fe', background: '#ffffff', surface: '#f4f4f5', text: '#18181b', textSecondary: '#71717a', border: '#e4e4e7', error: '#ef4444', warning: '#f59e0b', success: '#22c55e' }, spacing: SPACING, fonts: FONTS };
|
|
487
|
+
return { mode: 'dark', colors: { primary: '#6d28d9', secondary: '#1e1b4b', background: '#0a0a0f', surface: '#27272a', text: '#e4e4e7', textSecondary: '#a1a1aa', border: '#3f3f46', error: '#ef4444', warning: '#f59e0b', success: '#22c55e' }, spacing: SPACING, fonts: FONTS };
|
|
407
488
|
}
|
|
408
|
-
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
consoleCount.textContent = messageCount + ' messages';
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
let cls = 'log-info';
|
|
418
|
-
if (direction === 'error') cls = 'log-error';
|
|
419
|
-
if (direction === 'warn' || direction === 'cost') cls = 'log-warn';
|
|
420
|
-
|
|
421
|
-
let text = '<span class="log-time">' + time + '</span>';
|
|
422
|
-
text += '<span class="' + cls + '">[' + direction.toUpperCase() + '] ' + type + '</span>';
|
|
423
|
-
if (payload && typeof payload === 'object') {
|
|
424
|
-
text += ' <span style="color:#6b7280">' + JSON.stringify(payload).substring(0, 120) + '</span>';
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
entry.innerHTML = text;
|
|
428
|
-
consoleBody.appendChild(entry);
|
|
429
|
-
consoleBody.scrollTop = consoleBody.scrollHeight;
|
|
489
|
+
function checkPluginReachable(cb) { fetch('/api/check-plugin').then(function(r) { return r.json(); }).then(function(d) { cb(d.reachable); }).catch(function() { cb(true); }); }
|
|
490
|
+
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() {}); }
|
|
491
|
+
function logMessage(dir, type, payload) {
|
|
492
|
+
messageCount++; consoleCount.textContent = messageCount + ' messages';
|
|
493
|
+
var entry = document.createElement('div'); entry.className = 'log-entry';
|
|
494
|
+
var cls = 'log-info'; if (dir === 'error') cls = 'log-error'; if (dir === 'warn' || dir === 'cost' || dir === 'toast') cls = 'log-warn';
|
|
495
|
+
var text = '<span class="log-time">' + new Date().toLocaleTimeString() + '</span><span class="' + cls + '">[' + dir.toUpperCase() + '] ' + type + '</span>';
|
|
496
|
+
if (payload && typeof payload === 'object') text += ' <span style="color:#6b7280">' + JSON.stringify(payload).substring(0, 120) + '</span>';
|
|
497
|
+
entry.innerHTML = text; consoleBody.appendChild(entry); consoleBody.scrollTop = consoleBody.scrollHeight;
|
|
430
498
|
}
|
|
431
499
|
})();
|
|
432
500
|
`;
|