@fias/plugin-dev-harness 1.1.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.
Files changed (125) hide show
  1. package/dist/bridge/live-handler.d.ts +27 -0
  2. package/dist/bridge/live-handler.d.ts.map +1 -0
  3. package/dist/bridge/live-handler.js +47 -0
  4. package/dist/bridge/live-handler.js.map +1 -0
  5. package/dist/bridge/live-handler.test.d.ts +2 -0
  6. package/dist/bridge/live-handler.test.d.ts.map +1 -0
  7. package/dist/bridge/live-handler.test.js +182 -0
  8. package/dist/bridge/live-handler.test.js.map +1 -0
  9. package/dist/bridge/mock-handler.d.ts +25 -0
  10. package/dist/bridge/mock-handler.d.ts.map +1 -0
  11. package/dist/bridge/mock-handler.js +81 -0
  12. package/dist/bridge/mock-handler.js.map +1 -0
  13. package/dist/bridge/mock-handler.test.d.ts +2 -0
  14. package/dist/bridge/mock-handler.test.d.ts.map +1 -0
  15. package/dist/bridge/mock-handler.test.js +322 -0
  16. package/dist/bridge/mock-handler.test.js.map +1 -0
  17. package/dist/bridge/types.d.ts +18 -0
  18. package/dist/bridge/types.d.ts.map +1 -0
  19. package/dist/bridge/types.js +8 -0
  20. package/dist/bridge/types.js.map +1 -0
  21. package/dist/cli/dev.d.ts +15 -0
  22. package/dist/cli/dev.d.ts.map +1 -0
  23. package/dist/cli/dev.js +64 -0
  24. package/dist/cli/dev.js.map +1 -0
  25. package/dist/cli/dev.test.d.ts +2 -0
  26. package/dist/cli/dev.test.d.ts.map +1 -0
  27. package/dist/cli/dev.test.js +114 -0
  28. package/dist/cli/dev.test.js.map +1 -0
  29. package/dist/cli/entities.d.ts +9 -0
  30. package/dist/cli/entities.d.ts.map +1 -0
  31. package/dist/cli/entities.js +71 -0
  32. package/dist/cli/entities.js.map +1 -0
  33. package/dist/cli/entities.test.d.ts +2 -0
  34. package/dist/cli/entities.test.d.ts.map +1 -0
  35. package/dist/cli/entities.test.js +179 -0
  36. package/dist/cli/entities.test.js.map +1 -0
  37. package/dist/cli/index.d.ts +9 -0
  38. package/dist/cli/index.d.ts.map +1 -0
  39. package/dist/cli/index.js +29 -0
  40. package/dist/cli/index.js.map +1 -0
  41. package/dist/cli/index.test.d.ts +2 -0
  42. package/dist/cli/index.test.d.ts.map +1 -0
  43. package/dist/cli/index.test.js +55 -0
  44. package/dist/cli/index.test.js.map +1 -0
  45. package/dist/cli/login.d.ts +9 -0
  46. package/dist/cli/login.d.ts.map +1 -0
  47. package/dist/cli/login.js +80 -0
  48. package/dist/cli/login.js.map +1 -0
  49. package/dist/cli/login.test.d.ts +2 -0
  50. package/dist/cli/login.test.d.ts.map +1 -0
  51. package/dist/cli/login.test.js +53 -0
  52. package/dist/cli/login.test.js.map +1 -0
  53. package/dist/cli/submit.d.ts +9 -0
  54. package/dist/cli/submit.d.ts.map +1 -0
  55. package/dist/cli/submit.js +250 -0
  56. package/dist/cli/submit.js.map +1 -0
  57. package/dist/cli/submit.test.d.ts +2 -0
  58. package/dist/cli/submit.test.d.ts.map +1 -0
  59. package/dist/cli/submit.test.js +381 -0
  60. package/dist/cli/submit.test.js.map +1 -0
  61. package/dist/cli/validate.d.ts +9 -0
  62. package/dist/cli/validate.d.ts.map +1 -0
  63. package/dist/cli/validate.js +154 -0
  64. package/dist/cli/validate.js.map +1 -0
  65. package/dist/cli/validate.test.d.ts +2 -0
  66. package/dist/cli/validate.test.d.ts.map +1 -0
  67. package/dist/cli/validate.test.js +275 -0
  68. package/dist/cli/validate.test.js.map +1 -0
  69. package/dist/config/config-loader.d.ts +25 -0
  70. package/dist/config/config-loader.d.ts.map +1 -0
  71. package/dist/config/config-loader.js +78 -0
  72. package/dist/config/config-loader.js.map +1 -0
  73. package/dist/config/config-loader.test.d.ts +2 -0
  74. package/dist/config/config-loader.test.d.ts.map +1 -0
  75. package/dist/config/config-loader.test.js +163 -0
  76. package/dist/config/config-loader.test.js.map +1 -0
  77. package/dist/config/credentials.d.ts +11 -0
  78. package/dist/config/credentials.d.ts.map +1 -0
  79. package/dist/config/credentials.js +71 -0
  80. package/dist/config/credentials.js.map +1 -0
  81. package/dist/config/credentials.test.d.ts +2 -0
  82. package/dist/config/credentials.test.d.ts.map +1 -0
  83. package/dist/config/credentials.test.js +115 -0
  84. package/dist/config/credentials.test.js.map +1 -0
  85. package/dist/index.d.ts +9 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +12 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/mocks/entity-responses.d.ts +15 -0
  90. package/dist/mocks/entity-responses.d.ts.map +1 -0
  91. package/dist/mocks/entity-responses.js +21 -0
  92. package/dist/mocks/entity-responses.js.map +1 -0
  93. package/dist/mocks/entity-responses.test.d.ts +2 -0
  94. package/dist/mocks/entity-responses.test.d.ts.map +1 -0
  95. package/dist/mocks/entity-responses.test.js +53 -0
  96. package/dist/mocks/entity-responses.test.js.map +1 -0
  97. package/dist/mocks/theme.d.ts +22 -0
  98. package/dist/mocks/theme.d.ts.map +1 -0
  99. package/dist/mocks/theme.js +37 -0
  100. package/dist/mocks/theme.js.map +1 -0
  101. package/dist/mocks/theme.test.d.ts +2 -0
  102. package/dist/mocks/theme.test.d.ts.map +1 -0
  103. package/dist/mocks/theme.test.js +66 -0
  104. package/dist/mocks/theme.test.js.map +1 -0
  105. package/dist/mocks/user.d.ts +12 -0
  106. package/dist/mocks/user.d.ts.map +1 -0
  107. package/dist/mocks/user.js +14 -0
  108. package/dist/mocks/user.js.map +1 -0
  109. package/dist/mocks/user.test.d.ts +2 -0
  110. package/dist/mocks/user.test.d.ts.map +1 -0
  111. package/dist/mocks/user.test.js +23 -0
  112. package/dist/mocks/user.test.js.map +1 -0
  113. package/dist/server/harness-server.d.ts +24 -0
  114. package/dist/server/harness-server.d.ts.map +1 -0
  115. package/dist/server/harness-server.js +433 -0
  116. package/dist/server/harness-server.js.map +1 -0
  117. package/dist/server/harness-server.test.d.ts +2 -0
  118. package/dist/server/harness-server.test.d.ts.map +1 -0
  119. package/dist/server/harness-server.test.js +178 -0
  120. package/dist/server/harness-server.test.js.map +1 -0
  121. package/dist/server/static/harness.css +160 -0
  122. package/dist/server/static/harness.html +35 -0
  123. package/dist/server/static/harness.js +345 -0
  124. package/package.json +43 -0
  125. package/templates/fias-dev.config.json +12 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * FIAS Dev Harness — Client-side Bridge Host
3
+ *
4
+ * Ported from the production PluginBridgeHost
5
+ * (client/src/arche/components/PluginBridge.ts).
6
+ *
7
+ * Handles postMessage communication with the plugin iframe,
8
+ * enforcing permissions and rate limits, and proxying server-side
9
+ * operations through the harness Express server.
10
+ */
11
+ (function () {
12
+ 'use strict';
13
+
14
+ var iframe = document.getElementById('plugin-iframe');
15
+ var consoleBody = document.getElementById('console-body');
16
+ var consoleCount = document.getElementById('console-count');
17
+ var consoleToggle = document.getElementById('console-toggle');
18
+ var creditBalance = document.getElementById('credit-balance');
19
+ var themeToggle = document.getElementById('theme-toggle');
20
+ var reloadBtn = document.getElementById('reload-btn');
21
+ var modeBadge = document.getElementById('mode-badge');
22
+ var permissionsEl = document.getElementById('permissions');
23
+
24
+ var messageCount = 0;
25
+ var currentTheme = 'dark';
26
+ var cachedConfig = null;
27
+
28
+ /** Permission requirements per bridge call type (matches production) */
29
+ var PERMISSION_MAP = {
30
+ get_user: 'user:profile:read',
31
+ get_theme: 'theme:read',
32
+ entity_invoke: 'entities:invoke',
33
+ storage_read: 'storage:sandbox',
34
+ storage_write: 'storage:sandbox',
35
+ storage_list: 'storage:sandbox',
36
+ storage_delete: 'storage:sandbox',
37
+ };
38
+
39
+ /** Rate limits per message type (matches production) */
40
+ var RATE_LIMITS = {
41
+ entity_invoke: { maxPerMinute: 60 },
42
+ storage_write: { maxPerMinute: 120 },
43
+ storage_read: { maxPerMinute: 300 },
44
+ storage_list: { maxPerMinute: 60 },
45
+ storage_delete: { maxPerMinute: 60 },
46
+ };
47
+ var rateBuckets = {};
48
+
49
+ // ────────────────────────────────────────────────────────────────
50
+ // Initialization
51
+ // ────────────────────────────────────────────────────────────────
52
+
53
+ fetchConfig().then(function (config) {
54
+ cachedConfig = config;
55
+ currentTheme = config.mockTheme || 'dark';
56
+
57
+ // Update UI
58
+ modeBadge.textContent = config.isLive ? 'LIVE' : 'MOCK';
59
+ modeBadge.className = 'mode-badge ' + (config.isLive ? 'mode-live' : 'mode-mock');
60
+
61
+ permissionsEl.innerHTML = config.permissions
62
+ .map(function (p) {
63
+ return '<span class="perm-badge">' + escapeHtml(p) + '</span>';
64
+ })
65
+ .join('');
66
+
67
+ if (config.isLive) {
68
+ creditBalance.style.display = 'inline';
69
+ fetchCredits();
70
+ }
71
+
72
+ // Load plugin
73
+ iframe.src = config.pluginUrl;
74
+ });
75
+
76
+ // ────────────────────────────────────────────────────────────────
77
+ // UI Controls
78
+ // ────────────────────────────────────────────────────────────────
79
+
80
+ consoleToggle.addEventListener('click', function () {
81
+ consoleBody.classList.toggle('open');
82
+ });
83
+
84
+ reloadBtn.addEventListener('click', function () {
85
+ iframe.src = iframe.src;
86
+ });
87
+
88
+ themeToggle.addEventListener('click', function () {
89
+ currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
90
+ sendToPlugin({
91
+ type: 'theme_update',
92
+ messageId: 'theme_' + Date.now(),
93
+ payload: getTheme(),
94
+ });
95
+ logMessage('send', 'theme_update', { mode: currentTheme });
96
+ });
97
+
98
+ // ────────────────────────────────────────────────────────────────
99
+ // Message Handling
100
+ // ────────────────────────────────────────────────────────────────
101
+
102
+ window.addEventListener('message', function (event) {
103
+ if (event.source !== iframe.contentWindow) return;
104
+
105
+ var data = event.data;
106
+ if (!data || typeof data !== 'object' || !data.type || !data.messageId) return;
107
+
108
+ logMessage('recv', data.type, data.payload);
109
+
110
+ // Fire-and-forget messages
111
+ if (data.type === 'ready') return;
112
+
113
+ if (data.type === 'resize') {
114
+ var height = data.payload && data.payload.height;
115
+ if (typeof height === 'number' && height > 0) {
116
+ iframe.style.height = height + 'px';
117
+ iframe.style.flex = 'none';
118
+ }
119
+ return;
120
+ }
121
+
122
+ if (data.type === 'toast') {
123
+ var msg = data.payload && data.payload.message;
124
+ if (typeof msg === 'string') {
125
+ logMessage('toast', msg, data.payload);
126
+ }
127
+ return;
128
+ }
129
+
130
+ if (data.type === 'navigate') {
131
+ var navPath = data.payload && data.payload.path;
132
+ if (typeof navPath === 'string') {
133
+ logMessage('nav', navPath);
134
+ }
135
+ return;
136
+ }
137
+
138
+ // Request/response messages — enforce permissions + rate limits, then proxy
139
+ handleRequest(data);
140
+ });
141
+
142
+ function handleRequest(data) {
143
+ try {
144
+ // Check permissions
145
+ var requiredPerm = PERMISSION_MAP[data.type];
146
+ if (requiredPerm && cachedConfig && cachedConfig.permissions.indexOf(requiredPerm) === -1) {
147
+ throw new Error('Permission denied: ' + requiredPerm + ' not granted');
148
+ }
149
+
150
+ // Check rate limit
151
+ checkRateLimit(data.type);
152
+ } catch (err) {
153
+ logMessage('error', err.message);
154
+ sendToPlugin({
155
+ type: 'response',
156
+ messageId: data.messageId,
157
+ payload: null,
158
+ error: err.message,
159
+ });
160
+ return;
161
+ }
162
+
163
+ // Proxy to harness server
164
+ fetch('/api/bridge', {
165
+ method: 'POST',
166
+ headers: { 'Content-Type': 'application/json' },
167
+ body: JSON.stringify({ type: data.type, payload: data.payload }),
168
+ })
169
+ .then(function (response) {
170
+ if (!response.ok) {
171
+ return response.json().then(function (err) {
172
+ throw new Error(err.error || 'Bridge call failed');
173
+ });
174
+ }
175
+ return response.json();
176
+ })
177
+ .then(function (result) {
178
+ logMessage('send', 'response', result);
179
+
180
+ // Show cost for entity invocations in live mode
181
+ if (
182
+ data.type === 'entity_invoke' &&
183
+ result.metadata &&
184
+ result.metadata.cost > 0
185
+ ) {
186
+ logMessage('cost', 'Credits used: ' + result.metadata.cost.toFixed(4));
187
+ fetchCredits();
188
+ }
189
+
190
+ sendToPlugin({
191
+ type: 'response',
192
+ messageId: data.messageId,
193
+ payload: result,
194
+ });
195
+ })
196
+ .catch(function (err) {
197
+ logMessage('error', err.message);
198
+ sendToPlugin({
199
+ type: 'response',
200
+ messageId: data.messageId,
201
+ payload: null,
202
+ error: err.message,
203
+ });
204
+ });
205
+ }
206
+
207
+ // ────────────────────────────────────────────────────────────────
208
+ // Iframe Init
209
+ // ────────────────────────────────────────────────────────────────
210
+
211
+ iframe.addEventListener('load', function () {
212
+ if (!cachedConfig) return;
213
+
214
+ sendToPlugin({
215
+ type: 'init',
216
+ messageId: 'init_0',
217
+ payload: {
218
+ archId: 'dev_harness',
219
+ permissions: cachedConfig.permissions,
220
+ theme: getTheme(),
221
+ currentPath: '/',
222
+ },
223
+ });
224
+ logMessage('send', 'init');
225
+ });
226
+
227
+ // ────────────────────────────────────────────────────────────────
228
+ // Helpers
229
+ // ────────────────────────────────────────────────────────────────
230
+
231
+ function sendToPlugin(message) {
232
+ // targetOrigin '*' because sandboxed iframes have opaque origins.
233
+ // Security is enforced by checking event.source on incoming messages.
234
+ iframe.contentWindow && iframe.contentWindow.postMessage(message, '*');
235
+ }
236
+
237
+ function checkRateLimit(type) {
238
+ var limit = RATE_LIMITS[type];
239
+ if (!limit) return;
240
+
241
+ var now = Date.now();
242
+ var bucket = rateBuckets[type];
243
+
244
+ if (!bucket || now - bucket.windowStart > 60000) {
245
+ rateBuckets[type] = { count: 1, windowStart: now };
246
+ return;
247
+ }
248
+
249
+ if (bucket.count >= limit.maxPerMinute) {
250
+ throw new Error(
251
+ 'Rate limit exceeded for ' + type + ': max ' + limit.maxPerMinute + '/minute',
252
+ );
253
+ }
254
+
255
+ bucket.count++;
256
+ }
257
+
258
+ function getTheme() {
259
+ if (currentTheme === 'light') {
260
+ return {
261
+ mode: 'light',
262
+ colors: {
263
+ background: '#ffffff',
264
+ foreground: '#18181b',
265
+ primary: '#7c3aed',
266
+ secondary: '#ede9fe',
267
+ accent: '#8b5cf6',
268
+ muted: '#f4f4f5',
269
+ border: '#e4e4e7',
270
+ },
271
+ };
272
+ }
273
+ return {
274
+ mode: 'dark',
275
+ colors: {
276
+ background: '#0a0a0f',
277
+ foreground: '#e4e4e7',
278
+ primary: '#6d28d9',
279
+ secondary: '#1e1b4b',
280
+ accent: '#8b5cf6',
281
+ muted: '#27272a',
282
+ border: '#3f3f46',
283
+ },
284
+ };
285
+ }
286
+
287
+ function fetchConfig() {
288
+ return fetch('/api/config').then(function (r) {
289
+ return r.json();
290
+ });
291
+ }
292
+
293
+ function fetchCredits() {
294
+ fetch('/api/credits')
295
+ .then(function (r) {
296
+ return r.json();
297
+ })
298
+ .then(function (data) {
299
+ if (data.balance !== undefined && isFinite(data.balance)) {
300
+ creditBalance.textContent = 'Credits: ' + data.balance.toFixed(2);
301
+ }
302
+ })
303
+ .catch(function () {});
304
+ }
305
+
306
+ function logMessage(direction, type, payload) {
307
+ messageCount++;
308
+ consoleCount.textContent = messageCount + ' messages';
309
+
310
+ var entry = document.createElement('div');
311
+ entry.className = 'log-entry';
312
+
313
+ var time = new Date().toLocaleTimeString();
314
+ var cls = 'log-info';
315
+ if (direction === 'error') cls = 'log-error';
316
+ if (direction === 'warn' || direction === 'cost') cls = 'log-warn';
317
+ if (direction === 'toast') cls = 'log-warn';
318
+
319
+ var text = '<span class="log-time">' + escapeHtml(time) + '</span>';
320
+ text +=
321
+ '<span class="' +
322
+ cls +
323
+ '">[' +
324
+ escapeHtml(direction.toUpperCase()) +
325
+ '] ' +
326
+ escapeHtml(String(type)) +
327
+ '</span>';
328
+ if (payload && typeof payload === 'object') {
329
+ text +=
330
+ ' <span style="color:#6b7280">' +
331
+ escapeHtml(JSON.stringify(payload).substring(0, 120)) +
332
+ '</span>';
333
+ }
334
+
335
+ entry.innerHTML = text;
336
+ consoleBody.appendChild(entry);
337
+ consoleBody.scrollTop = consoleBody.scrollHeight;
338
+ }
339
+
340
+ function escapeHtml(str) {
341
+ var div = document.createElement('div');
342
+ div.appendChild(document.createTextNode(str));
343
+ return div.innerHTML;
344
+ }
345
+ })();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@fias/plugin-dev-harness",
3
+ "version": "1.1.0",
4
+ "description": "Development harness for building and testing FIAS plugin arches locally",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "fias-dev": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "templates",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc && cp -r src/server/static dist/server/static",
20
+ "clean": "rm -rf dist",
21
+ "watch": "tsc -w",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "keywords": [
25
+ "fias",
26
+ "plugin",
27
+ "arche",
28
+ "dev",
29
+ "harness"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "commander": "^12.0.0",
34
+ "express": "^4.21.0",
35
+ "open": "^10.0.0",
36
+ "chalk": "^4.1.2"
37
+ },
38
+ "devDependencies": {
39
+ "@types/express": "^5.0.0",
40
+ "@types/node": "^25.0.0",
41
+ "typescript": "^5.3.3"
42
+ }
43
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "pluginUrl": "http://localhost:3100",
3
+ "port": 3200,
4
+ "permissions": ["theme:read", "entities:invoke", "storage:sandbox", "user:profile:read"],
5
+ "mockUser": {
6
+ "userId": "dev_user_001",
7
+ "displayName": "Dev User",
8
+ "avatar": null
9
+ },
10
+ "mockTheme": "light",
11
+ "apiUrl": "https://api.fias.app/v1"
12
+ }