@wenrwa/marketplace-cli 0.6.4 → 0.6.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.
|
@@ -54,6 +54,21 @@ describe('agent list', () => {
|
|
|
54
54
|
spy.mockRestore();
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
describe('agent logs', () => {
|
|
58
|
+
// sio-07: `agent logs` streams via the SAME connectWebSocket() helper
|
|
59
|
+
// dashboard.ts uses, which is what carries the authenticated handshake
|
|
60
|
+
// (auth: { token }, path '/marketplace') — exercised directly in
|
|
61
|
+
// websocket.test.ts. This just pins that `agent logs` still wires through
|
|
62
|
+
// that one shared call site rather than opening its own socket.
|
|
63
|
+
it('streams via the shared authenticated websocket helper', async () => {
|
|
64
|
+
const { connectWebSocket } = require('../utils/websocket');
|
|
65
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
66
|
+
await runCommand('logs', 'wallet-abc-12345678');
|
|
67
|
+
expect(connectWebSocket).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(connectWebSocket).toHaveBeenCalledWith(expect.any(Function), { channel: 'agent', filters: { agentWallet: 'wallet-abc-12345678' } });
|
|
69
|
+
logSpy.mockRestore();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
57
72
|
describe('agent status', () => {
|
|
58
73
|
it('displays agent details', async () => {
|
|
59
74
|
mockClient.get.mockImplementation((path) => {
|
|
@@ -4,10 +4,35 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const chalk_1 = __importDefault(require("chalk"));
|
|
7
|
+
const events_1 = require("events");
|
|
8
|
+
// sio-07 (docs/prd/authenticated-sockets.md): the CLI is a headless client —
|
|
9
|
+
// it must authenticate the Socket.IO handshake with `auth: { token }`
|
|
10
|
+
// (never the retired `walletPubkey` field) against the server's
|
|
11
|
+
// `/marketplace` path (backend/src/server.ts), and surface a clear message
|
|
12
|
+
// on refusal. connectWebSocket() is the single call site both
|
|
13
|
+
// `dashboard.ts` and `agent.ts` (`agent logs`) share, so exercising it here
|
|
14
|
+
// covers the handshake contract for both commands.
|
|
15
|
+
const TEST_API_KEY = 'wm_sk_' + 'a'.repeat(64);
|
|
16
|
+
const mockIo = jest.fn();
|
|
17
|
+
jest.mock('socket.io-client', () => ({
|
|
18
|
+
io: (...args) => mockIo(...args),
|
|
19
|
+
}));
|
|
20
|
+
jest.mock('../utils/config', () => ({
|
|
21
|
+
requireConfig: jest.fn().mockReturnValue({
|
|
22
|
+
apiUrl: 'https://api.wenrwa.com/api/v1/api/marketplace',
|
|
23
|
+
apiKey: 'wm_sk_' + 'a'.repeat(64),
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
7
26
|
const websocket_1 = require("../utils/websocket");
|
|
27
|
+
class FakeSocket extends events_1.EventEmitter {
|
|
28
|
+
disconnect = jest.fn();
|
|
29
|
+
}
|
|
8
30
|
beforeAll(() => {
|
|
9
31
|
chalk_1.default.level = 0;
|
|
10
32
|
});
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
mockIo.mockReset();
|
|
35
|
+
});
|
|
11
36
|
function makeEvent(event, data, timestamp) {
|
|
12
37
|
return { event, data, timestamp: timestamp || '2024-06-15T14:30:00.000Z' };
|
|
13
38
|
}
|
|
@@ -59,3 +84,71 @@ describe('formatEventJson()', () => {
|
|
|
59
84
|
expect(result.event).toBe('test');
|
|
60
85
|
});
|
|
61
86
|
});
|
|
87
|
+
describe('connectWebSocket() handshake (sio-07)', () => {
|
|
88
|
+
it('authenticates with auth.token = the API key', () => {
|
|
89
|
+
const fake = new FakeSocket();
|
|
90
|
+
mockIo.mockReturnValue(fake);
|
|
91
|
+
(0, websocket_1.connectWebSocket)(() => { });
|
|
92
|
+
expect(mockIo).toHaveBeenCalledTimes(1);
|
|
93
|
+
const [, opts] = mockIo.mock.calls[0];
|
|
94
|
+
expect(opts.auth).toEqual({ token: TEST_API_KEY });
|
|
95
|
+
});
|
|
96
|
+
it('never sends the retired walletPubkey field', () => {
|
|
97
|
+
const fake = new FakeSocket();
|
|
98
|
+
mockIo.mockReturnValue(fake);
|
|
99
|
+
(0, websocket_1.connectWebSocket)(() => { });
|
|
100
|
+
const [, opts] = mockIo.mock.calls[0];
|
|
101
|
+
expect(opts.auth).not.toHaveProperty('walletPubkey');
|
|
102
|
+
expect(JSON.stringify(opts)).not.toContain('walletPubkey');
|
|
103
|
+
});
|
|
104
|
+
it('connects on the server\'s /marketplace Socket.IO path', () => {
|
|
105
|
+
const fake = new FakeSocket();
|
|
106
|
+
mockIo.mockReturnValue(fake);
|
|
107
|
+
(0, websocket_1.connectWebSocket)(() => { });
|
|
108
|
+
const [, opts] = mockIo.mock.calls[0];
|
|
109
|
+
expect(opts.path).toBe('/marketplace');
|
|
110
|
+
});
|
|
111
|
+
it('shows a clear message and tells the user to log in again on CREDENTIAL_INVALID', () => {
|
|
112
|
+
const fake = new FakeSocket();
|
|
113
|
+
mockIo.mockReturnValue(fake);
|
|
114
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
115
|
+
(0, websocket_1.connectWebSocket)(() => { });
|
|
116
|
+
const err = new Error('invalid credential');
|
|
117
|
+
err.data = { code: 'CREDENTIAL_INVALID' };
|
|
118
|
+
fake.emit('connect_error', err);
|
|
119
|
+
const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
120
|
+
expect(printed).toMatch(/wen-market auth login/);
|
|
121
|
+
errorSpy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
it('shows a clear message and tells the user to log in again on SESSION_REVOKED', () => {
|
|
124
|
+
const fake = new FakeSocket();
|
|
125
|
+
mockIo.mockReturnValue(fake);
|
|
126
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
127
|
+
(0, websocket_1.connectWebSocket)(() => { });
|
|
128
|
+
const err = new Error('session revoked');
|
|
129
|
+
err.data = { code: 'SESSION_REVOKED' };
|
|
130
|
+
fake.emit('connect_error', err);
|
|
131
|
+
const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
132
|
+
expect(printed).toMatch(/wen-market auth login/);
|
|
133
|
+
errorSpy.mockRestore();
|
|
134
|
+
});
|
|
135
|
+
it('falls back to a generic message for other connect errors', () => {
|
|
136
|
+
const fake = new FakeSocket();
|
|
137
|
+
mockIo.mockReturnValue(fake);
|
|
138
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
139
|
+
(0, websocket_1.connectWebSocket)(() => { });
|
|
140
|
+
fake.emit('connect_error', new Error('ECONNREFUSED'));
|
|
141
|
+
const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
142
|
+
expect(printed).toContain('ECONNREFUSED');
|
|
143
|
+
expect(printed).not.toMatch(/wen-market auth login/);
|
|
144
|
+
errorSpy.mockRestore();
|
|
145
|
+
});
|
|
146
|
+
it('subscribes to the requested channel on connect, unaffected by the auth change', () => {
|
|
147
|
+
const fake = new FakeSocket();
|
|
148
|
+
mockIo.mockReturnValue(fake);
|
|
149
|
+
const emitSpy = jest.spyOn(fake, 'emit');
|
|
150
|
+
(0, websocket_1.connectWebSocket)(() => { }, { channel: 'workspace', filters: { workspaceId: 'ws1' } });
|
|
151
|
+
fake.emit('connect');
|
|
152
|
+
expect(emitSpy).toHaveBeenCalledWith('subscribe', { channel: 'workspace', workspaceId: 'ws1' });
|
|
153
|
+
});
|
|
154
|
+
});
|
package/dist/utils/websocket.js
CHANGED
|
@@ -10,12 +10,32 @@ const socket_io_client_1 = require("socket.io-client");
|
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
11
|
const config_1 = require("./config");
|
|
12
12
|
const format_1 = require("./format");
|
|
13
|
+
// sio-07 (docs/prd/authenticated-sockets.md): headless clients (the CLI)
|
|
14
|
+
// authenticate the Socket.IO handshake with `auth: { token }` — the API key
|
|
15
|
+
// (`wm_sk_…`) — against the server's dedicated `/marketplace` path
|
|
16
|
+
// (backend/src/server.ts). The legacy `auth: { walletPubkey }` field proved
|
|
17
|
+
// nothing (public keys are public) and is retired; the server's
|
|
18
|
+
// socket-auth middleware (backend/src/middleware/socket-auth.middleware.ts)
|
|
19
|
+
// refuses bad/revoked credentials with a `connect_error` carrying
|
|
20
|
+
// `err.data.code` of CREDENTIAL_INVALID or SESSION_REVOKED.
|
|
21
|
+
const AUTH_LOGIN_HINT = 'Run `wen-market auth login` to sign in again.';
|
|
22
|
+
function connectRefusalMessage(err) {
|
|
23
|
+
const code = err?.data?.code;
|
|
24
|
+
if (code === 'CREDENTIAL_INVALID') {
|
|
25
|
+
return `Your API key is invalid or expired. ${AUTH_LOGIN_HINT}`;
|
|
26
|
+
}
|
|
27
|
+
if (code === 'SESSION_REVOKED') {
|
|
28
|
+
return `Your session was revoked. ${AUTH_LOGIN_HINT}`;
|
|
29
|
+
}
|
|
30
|
+
return `WebSocket error: ${err?.message}`;
|
|
31
|
+
}
|
|
13
32
|
function connectWebSocket(onEvent, options = {}) {
|
|
14
33
|
const config = (0, config_1.requireConfig)();
|
|
15
34
|
const wsUrl = config.apiUrl.replace('/api/marketplace', '');
|
|
16
35
|
const socket = (0, socket_io_client_1.io)(wsUrl, {
|
|
36
|
+
path: '/marketplace',
|
|
17
37
|
transports: ['websocket'],
|
|
18
|
-
auth: {
|
|
38
|
+
auth: { token: config.apiKey },
|
|
19
39
|
});
|
|
20
40
|
socket.on('connect', () => {
|
|
21
41
|
if (options.channel) {
|
|
@@ -29,7 +49,7 @@ function connectWebSocket(onEvent, options = {}) {
|
|
|
29
49
|
onEvent(event);
|
|
30
50
|
});
|
|
31
51
|
socket.on('connect_error', (err) => {
|
|
32
|
-
console.error(chalk_1.default.red(
|
|
52
|
+
console.error(chalk_1.default.red(connectRefusalMessage(err)));
|
|
33
53
|
});
|
|
34
54
|
return socket;
|
|
35
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|