@harness-fe/runtime 4.0.0-next.4 → 4.0.0-next.5
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/client.js +2 -2
- package/dist/dashboardUrl.d.ts +7 -10
- package/dist/dashboardUrl.js +3 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -2
- package/src/client.ts +2 -2
- package/src/dashboardUrl.test.ts +16 -35
- package/src/dashboardUrl.ts +10 -17
- package/src/overlay.test.ts +6 -5
- package/src/runtimeClient.e2e.test.ts +16 -12
- package/src/version.ts +1 -1
package/dist/client.js
CHANGED
|
@@ -123,7 +123,7 @@ export class RuntimeClient {
|
|
|
123
123
|
this.recorder = new RrwebRecorder((chunk) => this.sendEvent(EVENT_NAME.RRWEB, chunk), { checkoutEveryNms: opts.rrwebCheckoutEveryNms });
|
|
124
124
|
}
|
|
125
125
|
start() {
|
|
126
|
-
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
126
|
+
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
127
127
|
this.ctx.capture.install((name, payload) => this.sendEvent(name, payload), { daemonUrl });
|
|
128
128
|
this.recorder.start();
|
|
129
129
|
this.connect();
|
|
@@ -134,7 +134,7 @@ export class RuntimeClient {
|
|
|
134
134
|
this.ws?.close();
|
|
135
135
|
}
|
|
136
136
|
connect() {
|
|
137
|
-
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
137
|
+
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
138
138
|
try {
|
|
139
139
|
this.ws = new WebSocket(url);
|
|
140
140
|
}
|
package/dist/dashboardUrl.d.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convert the runtime's `mcpUrl` (
|
|
3
|
-
* the
|
|
2
|
+
* Convert the runtime's `mcpUrl` (the gateway WebSocket URL the plugin gave us)
|
|
3
|
+
* into the console URL the same gateway serves: `<http>://<host>:<port>/console`,
|
|
4
|
+
* deep-linking to `/console/sessions/:id` when a sessionId is given.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `packages/mcp-server/src/dashboardSpa.ts`).
|
|
10
|
-
*
|
|
11
|
-
* Optionally deep-links into a session's detail page when `sessionId` is
|
|
12
|
-
* provided.
|
|
6
|
+
* This is a **pure shortcut** — it carries NO token. Opening it is plain
|
|
7
|
+
* navigation; the console authorizes the viewer on its own (admin sign-in or a
|
|
8
|
+
* pasted read token). The runtime's token is write-only and must never be used
|
|
9
|
+
* as a console auth grant, so it's deliberately omitted.
|
|
13
10
|
*/
|
|
14
11
|
export interface DashboardUrlInput {
|
|
15
12
|
mcpUrl: string;
|
package/dist/dashboardUrl.js
CHANGED
|
@@ -10,11 +10,7 @@ export function deriveDashboardUrl(input) {
|
|
|
10
10
|
}
|
|
11
11
|
const httpScheme = url.protocol === 'wss:' ? 'https:' : 'http:';
|
|
12
12
|
const path = input.sessionId
|
|
13
|
-
? `/
|
|
14
|
-
: '/
|
|
15
|
-
|
|
16
|
-
const search = token ? `?token=${encodeURIComponent(token)}` : '';
|
|
17
|
-
// Build manually so we don't leak any extra query/hash from the WS URL
|
|
18
|
-
// (rare, but be defensive — the agent only ever sees what we hand it).
|
|
19
|
-
return `${httpScheme}//${url.host}${path}${search}`;
|
|
13
|
+
? `/console/sessions/${encodeURIComponent(input.sessionId)}`
|
|
14
|
+
: '/console';
|
|
15
|
+
return `${httpScheme}//${url.host}${path}`;
|
|
20
16
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "4.0.0-next.
|
|
1
|
+
export declare const VERSION = "4.0.0-next.5";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-fe/runtime",
|
|
3
|
-
"version": "4.0.0-next.
|
|
3
|
+
"version": "4.0.0-next.5",
|
|
4
4
|
"description": "Browser-side SDK injected into the dev page. Connects to the MCP server via WebSocket and executes commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"happy-dom": "^20.9.0",
|
|
38
38
|
"typescript": "^5.6.0",
|
|
39
|
-
"vitest": "^2.1.9"
|
|
39
|
+
"vitest": "^2.1.9",
|
|
40
|
+
"@harness-fe/core": "4.0.0-next.5",
|
|
41
|
+
"@harness-fe/gateway": "4.0.0-next.5"
|
|
40
42
|
},
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"access": "public"
|
package/src/client.ts
CHANGED
|
@@ -190,7 +190,7 @@ export class RuntimeClient {
|
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
start(): void {
|
|
193
|
-
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
193
|
+
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
194
194
|
this.ctx.capture.install(
|
|
195
195
|
(name, payload) => this.sendEvent(name, payload),
|
|
196
196
|
{ daemonUrl },
|
|
@@ -206,7 +206,7 @@ export class RuntimeClient {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
private connect(): void {
|
|
209
|
-
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
209
|
+
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
210
210
|
try {
|
|
211
211
|
this.ws = new WebSocket(url);
|
|
212
212
|
} catch (err) {
|
package/src/dashboardUrl.test.ts
CHANGED
|
@@ -1,55 +1,36 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { deriveDashboardUrl } from './dashboardUrl.js';
|
|
3
3
|
|
|
4
|
-
describe('deriveDashboardUrl', () => {
|
|
5
|
-
it('swaps ws:// to http:// and points at /
|
|
6
|
-
expect(deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729' })).toBe(
|
|
7
|
-
'http://127.0.0.1:47729/
|
|
4
|
+
describe('deriveDashboardUrl (pure shortcut — no token)', () => {
|
|
5
|
+
it('swaps ws:// to http:// and points at /console', () => {
|
|
6
|
+
expect(deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws' })).toBe(
|
|
7
|
+
'http://127.0.0.1:47729/console',
|
|
8
8
|
);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('swaps wss:// to https:// (production / LAN with TLS)', () => {
|
|
12
|
-
expect(deriveDashboardUrl({ mcpUrl: 'wss://harness.lan:47729' })).toBe(
|
|
13
|
-
'https://harness.lan:47729/
|
|
12
|
+
expect(deriveDashboardUrl({ mcpUrl: 'wss://harness.lan:47729/ws' })).toBe(
|
|
13
|
+
'https://harness.lan:47729/console',
|
|
14
14
|
);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it('
|
|
17
|
+
it('deep-links to /console/sessions/:id when sessionId is provided', () => {
|
|
18
18
|
expect(
|
|
19
|
-
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729?token=abc' }),
|
|
20
|
-
).toBe('http://127.0.0.1:47729/
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('URL-encodes the token (defensive against weird HARNESS_FE_TOKEN values)', () => {
|
|
24
|
-
expect(
|
|
25
|
-
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729?token=a%20b%26c' }),
|
|
26
|
-
).toBe('http://127.0.0.1:47729/dashboard/?token=a%20b%26c');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('deep-links to /dashboard/sessions/:id when sessionId is provided', () => {
|
|
30
|
-
expect(
|
|
31
|
-
deriveDashboardUrl({
|
|
32
|
-
mcpUrl: 'ws://127.0.0.1:47729?token=abc',
|
|
33
|
-
sessionId: 'sess-1',
|
|
34
|
-
}),
|
|
35
|
-
).toBe('http://127.0.0.1:47729/dashboard/sessions/sess-1?token=abc');
|
|
19
|
+
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=abc', sessionId: 'sess-1' }),
|
|
20
|
+
).toBe('http://127.0.0.1:47729/console/sessions/sess-1');
|
|
36
21
|
});
|
|
37
22
|
|
|
38
23
|
it('URL-encodes the session id', () => {
|
|
39
24
|
expect(
|
|
40
|
-
deriveDashboardUrl({
|
|
41
|
-
|
|
42
|
-
sessionId: 'a/b c',
|
|
43
|
-
}),
|
|
44
|
-
).toBe('http://127.0.0.1:47729/dashboard/sessions/a%2Fb%20c');
|
|
25
|
+
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws', sessionId: 'a/b c' }),
|
|
26
|
+
).toBe('http://127.0.0.1:47729/console/sessions/a%2Fb%20c');
|
|
45
27
|
});
|
|
46
28
|
|
|
47
|
-
it('
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
).toBe('http://127.0.0.1:47729/dashboard/?token=abc');
|
|
29
|
+
it('never carries the runtime token — it is a navigation shortcut, not an auth grant', () => {
|
|
30
|
+
const url = deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=secret&other=x#h', sessionId: 'sess-1' });
|
|
31
|
+
expect(url).toBe('http://127.0.0.1:47729/console/sessions/sess-1');
|
|
32
|
+
expect(url).not.toContain('token');
|
|
33
|
+
expect(url).not.toContain('secret');
|
|
53
34
|
});
|
|
54
35
|
|
|
55
36
|
it('returns undefined for empty or invalid input', () => {
|
package/src/dashboardUrl.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convert the runtime's `mcpUrl` (
|
|
3
|
-
* the
|
|
2
|
+
* Convert the runtime's `mcpUrl` (the gateway WebSocket URL the plugin gave us)
|
|
3
|
+
* into the console URL the same gateway serves: `<http>://<host>:<port>/console`,
|
|
4
|
+
* deep-linking to `/console/sessions/:id` when a sessionId is given.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `packages/mcp-server/src/dashboardSpa.ts`).
|
|
10
|
-
*
|
|
11
|
-
* Optionally deep-links into a session's detail page when `sessionId` is
|
|
12
|
-
* provided.
|
|
6
|
+
* This is a **pure shortcut** — it carries NO token. Opening it is plain
|
|
7
|
+
* navigation; the console authorizes the viewer on its own (admin sign-in or a
|
|
8
|
+
* pasted read token). The runtime's token is write-only and must never be used
|
|
9
|
+
* as a console auth grant, so it's deliberately omitted.
|
|
13
10
|
*/
|
|
14
11
|
export interface DashboardUrlInput {
|
|
15
12
|
mcpUrl: string;
|
|
@@ -26,11 +23,7 @@ export function deriveDashboardUrl(input: DashboardUrlInput): string | undefined
|
|
|
26
23
|
}
|
|
27
24
|
const httpScheme = url.protocol === 'wss:' ? 'https:' : 'http:';
|
|
28
25
|
const path = input.sessionId
|
|
29
|
-
? `/
|
|
30
|
-
: '/
|
|
31
|
-
|
|
32
|
-
const search = token ? `?token=${encodeURIComponent(token)}` : '';
|
|
33
|
-
// Build manually so we don't leak any extra query/hash from the WS URL
|
|
34
|
-
// (rare, but be defensive — the agent only ever sees what we hand it).
|
|
35
|
-
return `${httpScheme}//${url.host}${path}${search}`;
|
|
26
|
+
? `/console/sessions/${encodeURIComponent(input.sessionId)}`
|
|
27
|
+
: '/console';
|
|
28
|
+
return `${httpScheme}//${url.host}${path}`;
|
|
36
29
|
}
|
package/src/overlay.test.ts
CHANGED
|
@@ -220,12 +220,13 @@ describe('installOverlay', () => {
|
|
|
220
220
|
|
|
221
221
|
it('shows the "Open dashboard" button only when the client has an mcpUrl', () => {
|
|
222
222
|
setupDom();
|
|
223
|
-
installOverlay(makeFakeClient({ mcpUrl: 'ws://127.0.0.1:47729?token=demo' }));
|
|
223
|
+
installOverlay(makeFakeClient({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=demo' }));
|
|
224
224
|
const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
|
|
225
225
|
const btn = root.querySelector('[data-role=open-dashboard]') as HTMLButtonElement;
|
|
226
226
|
expect(btn.style.display).toBe('');
|
|
227
|
-
expect(btn.title).toContain('http://127.0.0.1:47729/
|
|
228
|
-
|
|
227
|
+
expect(btn.title).toContain('http://127.0.0.1:47729/console/sessions/');
|
|
228
|
+
// The overlay link is a pure shortcut — it must NOT carry the runtime token.
|
|
229
|
+
expect(btn.title).not.toContain('token=');
|
|
229
230
|
});
|
|
230
231
|
|
|
231
232
|
it('hides the "Open dashboard" button when mcpUrl is missing', () => {
|
|
@@ -238,7 +239,7 @@ describe('installOverlay', () => {
|
|
|
238
239
|
|
|
239
240
|
it('clicking "Open dashboard" calls window.open with the derived URL in a new tab', () => {
|
|
240
241
|
setupDom();
|
|
241
|
-
installOverlay(makeFakeClient({ mcpUrl: 'wss://harness.lan:8443?token=t' }));
|
|
242
|
+
installOverlay(makeFakeClient({ mcpUrl: 'wss://harness.lan:8443/ws?token=t' }));
|
|
242
243
|
const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
|
|
243
244
|
const calls: Array<{ url: string; target: string; features: string }> = [];
|
|
244
245
|
(globalThis.window as unknown as { open: typeof window.open }).open = ((
|
|
@@ -252,7 +253,7 @@ describe('installOverlay', () => {
|
|
|
252
253
|
const btn = root.querySelector('[data-role=open-dashboard]') as HTMLButtonElement;
|
|
253
254
|
btn.click();
|
|
254
255
|
expect(calls).toHaveLength(1);
|
|
255
|
-
expect(calls[0].url).toBe('https://harness.lan:8443/
|
|
256
|
+
expect(calls[0].url).toBe('https://harness.lan:8443/console/sessions/sess-12345-abcdef-9876');
|
|
256
257
|
expect(calls[0].target).toBe('_blank');
|
|
257
258
|
expect(calls[0].features).toMatch(/noopener/);
|
|
258
259
|
});
|
|
@@ -24,15 +24,15 @@ vi.mock('rrweb', () => ({
|
|
|
24
24
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
25
25
|
import { tmpdir } from 'node:os';
|
|
26
26
|
import { join } from 'node:path';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { InProcessCoreClient, JsonlStore, type StoreEvent } from '@harness-fe/core';
|
|
28
|
+
import { createGateway, Policy, type GatewayHandle } from '@harness-fe/gateway';
|
|
29
29
|
import { RuntimeClient } from './client.js';
|
|
30
30
|
import { getCaptureStore } from './capture.js';
|
|
31
|
-
import type { StoreEvent } from '../../daemon/src/store/index.js';
|
|
32
31
|
import type { NetworkEntry, StorageEntry, WsEntry } from '@harness-fe/protocol';
|
|
33
32
|
|
|
34
33
|
interface Env {
|
|
35
|
-
|
|
34
|
+
core: InProcessCoreClient;
|
|
35
|
+
gw: GatewayHandle;
|
|
36
36
|
store: JsonlStore;
|
|
37
37
|
dir: string;
|
|
38
38
|
port: number;
|
|
@@ -59,9 +59,12 @@ async function rmDirWithRetry(dir: string, attempts = 5): Promise<void> {
|
|
|
59
59
|
async function setup(): Promise<Env> {
|
|
60
60
|
const dir = mkdtempSync(join(tmpdir(), 'harness-rt-e2e-'));
|
|
61
61
|
const store = new JsonlStore(dir);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
62
|
+
// New architecture: an in-process core behind the gateway front door; the
|
|
63
|
+
// runtime connects to the gateway's /ws (Open policy → local principal).
|
|
64
|
+
const core = new InProcessCoreClient({ store, taskStore: null, autoPurge: { enabled: false } });
|
|
65
|
+
await core.start();
|
|
66
|
+
const gw = createGateway({ coreClient: core, policy: new Policy({ mode: 'open' }) });
|
|
67
|
+
const port = await gw.listen(0, '127.0.0.1');
|
|
65
68
|
if (!port) throw new Error('no port');
|
|
66
69
|
|
|
67
70
|
// happy-dom keeps singletons across tests — reset the patch state so this
|
|
@@ -70,7 +73,7 @@ async function setup(): Promise<Env> {
|
|
|
70
73
|
|
|
71
74
|
const client = new RuntimeClient({
|
|
72
75
|
projectId: 'rt-e2e',
|
|
73
|
-
mcpUrl: `ws://127.0.0.1:${port}`,
|
|
76
|
+
mcpUrl: `ws://127.0.0.1:${port}/ws`,
|
|
74
77
|
});
|
|
75
78
|
client.start();
|
|
76
79
|
|
|
@@ -88,7 +91,7 @@ async function setup(): Promise<Env> {
|
|
|
88
91
|
throw new Error('runtime-client never connected');
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
return {
|
|
94
|
+
return { core, gw, store, dir, port, client, sessionId: client.sessionId };
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
beforeEach(async () => {
|
|
@@ -98,7 +101,8 @@ beforeEach(async () => {
|
|
|
98
101
|
afterEach(async () => {
|
|
99
102
|
if (!env) return;
|
|
100
103
|
env.client.stop();
|
|
101
|
-
await env.
|
|
104
|
+
await env.gw.close();
|
|
105
|
+
await env.core.stop();
|
|
102
106
|
// close() drains the async write queue — must await, else rmSync races
|
|
103
107
|
// file writes and the dir-recursive-rm trips ENOTEMPTY on Linux CI.
|
|
104
108
|
await env.store.close();
|
|
@@ -186,7 +190,7 @@ describe('RuntimeClient E2E — patched WebSocket flows to bridge', () => {
|
|
|
186
190
|
(window as unknown as { WebSocket: typeof WebSocket }).WebSocket = FakeWS as unknown as typeof WebSocket;
|
|
187
191
|
cap.install(
|
|
188
192
|
(name, payload) => e.client.sendEvent(name, payload),
|
|
189
|
-
{ daemonUrl: `ws://127.0.0.1:${e.port}` },
|
|
193
|
+
{ daemonUrl: `ws://127.0.0.1:${e.port}/ws` },
|
|
190
194
|
);
|
|
191
195
|
|
|
192
196
|
try {
|
|
@@ -245,7 +249,7 @@ describe('RuntimeClient E2E — patched fetch initiator round-trip', () => {
|
|
|
245
249
|
cap.dispose();
|
|
246
250
|
cap.install(
|
|
247
251
|
(name, payload) => e.client.sendEvent(name, payload),
|
|
248
|
-
{ daemonUrl: `ws://127.0.0.1:${e.port}` },
|
|
252
|
+
{ daemonUrl: `ws://127.0.0.1:${e.port}/ws` },
|
|
249
253
|
);
|
|
250
254
|
|
|
251
255
|
try {
|
package/src/version.ts
CHANGED