@fias/plugin-dev-harness 1.3.1 → 1.4.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.
- package/dist/config/credentials.d.ts +26 -11
- package/dist/config/credentials.d.ts.map +1 -1
- package/dist/config/credentials.js +57 -21
- package/dist/config/credentials.js.map +1 -1
- package/dist/config/credentials.test.js +98 -74
- package/dist/config/credentials.test.js.map +1 -1
- package/dist/server/harness-server.d.ts.map +1 -1
- package/dist/server/harness-server.js +110 -371
- package/dist/server/harness-server.js.map +1 -1
- package/dist/server/harness-server.test.js +8 -37
- package/dist/server/harness-server.test.js.map +1 -1
- package/dist/server/static/static/harness.html +8 -17
- package/dist/server/static/static/harness.js +27 -130
- package/package.json +1 -1
|
@@ -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
|
|
62
|
-
let
|
|
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 =
|
|
72
|
+
let liveHandler = currentAuthToken
|
|
77
73
|
? new live_handler_1.LiveBridgeHandler({
|
|
78
|
-
apiKey:
|
|
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 (
|
|
85
|
+
if (currentAuthToken) {
|
|
90
86
|
liveHandler = new live_handler_1.LiveBridgeHandler({
|
|
91
|
-
apiKey:
|
|
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: '
|
|
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
|
-
//
|
|
119
|
-
app.post('/api/login', (req, res) => {
|
|
120
|
-
const {
|
|
121
|
-
if (!
|
|
122
|
-
res.status(400).json({ error: '
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
res
|
|
150
|
-
.
|
|
151
|
-
|
|
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
|
-
|
|
193
|
+
currentAuthToken = (0, credentials_1.loadAuthTokenForEnvironment)(currentEnvironment);
|
|
194
|
+
currentSession = (0, credentials_1.loadSessionForEnvironment)(currentEnvironment);
|
|
171
195
|
rebuildLiveHandler();
|
|
172
|
-
// Fall back to mock if no
|
|
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(
|
|
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' || !
|
|
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 ${
|
|
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
|
|
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
|
|
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(
|
|
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>
|
|
348
|
-
<p>
|
|
349
|
-
<
|
|
350
|
-
|
|
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 & 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
|
|
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:
|
|
421
|
-
.modal-
|
|
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: #
|
|
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;
|
|
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
|
-
.
|
|
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
|