claude-flow 3.46.1 → 3.47.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/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/federation.js +4 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/x-federation-join.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/x-federation-join.js +61 -18
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -13,6 +13,8 @@ async function run(ctx, tool, args, title) {
|
|
|
13
13
|
// CLI flag --gateway takes precedence over the RUFLO_X_GATEWAY_URL env var (ADR-125).
|
|
14
14
|
const data = await callMCPTool(tool, { gatewayUrl: ctx.flags.gateway, ...args });
|
|
15
15
|
printJsonOrTable(ctx, data, title);
|
|
16
|
+
if (data && typeof data === 'object' && (('ok' in data && data.ok === false) || ('degraded' in data && data.degraded === true)))
|
|
17
|
+
return { success: false, exitCode: 1, data };
|
|
16
18
|
return { success: true, data };
|
|
17
19
|
}
|
|
18
20
|
catch (e) {
|
|
@@ -28,8 +30,8 @@ export const federationCommand = {
|
|
|
28
30
|
{ name: 'gateway', description: 'Gateway base URL (takes precedence over RUFLO_X_GATEWAY_URL; default https://x.ruv.io)', type: 'string' },
|
|
29
31
|
],
|
|
30
32
|
subcommands: [
|
|
31
|
-
{ name: 'join', description: 'Join the open swarm with YOUR OWN key
|
|
32
|
-
options: [{ name: 'code', description: 'Invite code (v2.…) — a bearer secret, keep it private', type: 'string'
|
|
33
|
+
{ name: 'join', description: 'Join the open swarm with YOUR OWN key without an invite on open relays (generates ~/.ruflo/nostr.key if absent, registers via NIP-98, verifies via NIP-42)',
|
|
34
|
+
options: [{ name: 'code', description: 'Invite code (v2.…) — a bearer secret, keep it private', type: 'string' }],
|
|
33
35
|
action: (ctx) => run(ctx, 'x_federation_join', { code: ctx.flags.code }, 'Join federation') },
|
|
34
36
|
{ name: 'sync', description: 'Fetch recent verified swarm messages',
|
|
35
37
|
options: [{ name: 'since', description: 'Look-back seconds (default 3600)', type: 'number' }, { name: 'limit', description: 'Max messages', type: 'number' }, { name: 'type', description: 'Filter by message type', type: 'string' }],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-service join
|
|
2
|
+
* Self-service federation join with a local key: public registration or a private invite.
|
|
3
3
|
*
|
|
4
4
|
* Decentralized by design: the user generates/holds THEIR OWN Nostr key locally,
|
|
5
|
-
*
|
|
5
|
+
* registers with a NIP-98-signed request (no admin in the user flow), proves
|
|
6
6
|
* membership with NIP-42, and announces themselves. The gateway never signs for them.
|
|
7
7
|
*
|
|
8
8
|
* `nostr-tools` is an optional dependency (secp256k1/Schnorr is not in node:crypto):
|
|
@@ -36,49 +36,92 @@ export function nip98Header(nt, sk, url, method, body) {
|
|
|
36
36
|
export async function verifyMembership(nt, sk, relayWs) {
|
|
37
37
|
const { default: WebSocket } = await import('ws');
|
|
38
38
|
return new Promise((resolve) => {
|
|
39
|
-
const ws = new WebSocket(relayWs, { perMessageDeflate: false });
|
|
40
|
-
let done = false;
|
|
39
|
+
const ws = new WebSocket(relayWs, { perMessageDeflate: false, maxPayload: 64 * 1024 });
|
|
40
|
+
let done = false, authId;
|
|
41
41
|
const fin = (r) => { if (done)
|
|
42
|
-
return; done = true; try {
|
|
42
|
+
return; done = true; clearTimeout(timer); try {
|
|
43
43
|
ws.close();
|
|
44
44
|
}
|
|
45
45
|
catch { /* */ } resolve(r); };
|
|
46
|
+
const timer = setTimeout(() => fin({ ok: false, reason: 'timeout' }), 15000);
|
|
46
47
|
ws.on('message', (d) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
let m;
|
|
49
|
+
try {
|
|
50
|
+
m = JSON.parse(d.toString());
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(m))
|
|
56
|
+
return;
|
|
57
|
+
if (m[0] === 'AUTH' && typeof m[1] === 'string' && m[1].length <= 1024 && !authId) {
|
|
58
|
+
const ev = nt.finalizeEvent({ kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [['relay', relayWs], ['challenge', m[1]]], content: '' }, sk);
|
|
59
|
+
authId = ev.id;
|
|
60
|
+
ws.send(JSON.stringify(['AUTH', ev]));
|
|
61
|
+
}
|
|
62
|
+
else if (m[0] === 'OK' && authId && m[1] === authId)
|
|
63
|
+
fin({ ok: m[2] === true, reason: m[3] });
|
|
52
64
|
});
|
|
53
65
|
ws.on('error', (e) => fin({ ok: false, reason: e.message }));
|
|
54
|
-
|
|
66
|
+
ws.on('close', () => fin({ ok: false, reason: 'closed before authentication' }));
|
|
55
67
|
});
|
|
56
68
|
}
|
|
69
|
+
function secureBase(value) {
|
|
70
|
+
const u = new URL(value);
|
|
71
|
+
if ((u.protocol !== 'https:' && !(u.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(u.hostname)))
|
|
72
|
+
|| u.username || u.password || u.search || u.hash || u.pathname !== '/')
|
|
73
|
+
throw new Error('gateway must be an HTTPS origin (HTTP allowed only for loopback tests)');
|
|
74
|
+
return u.origin;
|
|
75
|
+
}
|
|
57
76
|
export const xFederationJoinTools = [{
|
|
58
77
|
name: 'x_federation_join',
|
|
59
|
-
description: 'Join the
|
|
78
|
+
description: 'Join the federation with YOUR OWN local key, without an invite when public registration is enabled. Reuses ~/.ruflo/nostr.key (0600), proves key ownership with NIP-98, and verifies membership with NIP-42. Use when a user wants to join as themselves. Publishing as the gateway is wrong for personal identity. Optional private invite codes remain supported; never share keys or codes in chat.',
|
|
60
79
|
inputSchema: { type: 'object', properties: {
|
|
61
|
-
code: { type: 'string', description: '
|
|
80
|
+
code: { type: 'string', description: 'Optional private invite code (v2.…), for invite-only relays.' },
|
|
81
|
+
gatewayUrl: { type: 'string', description: 'Public registration gateway origin; defaults to https://x.ruv.io.' },
|
|
62
82
|
relayHttp: { type: 'string', description: 'Relay HTTPS base for the claim; takes precedence over RUFLO_X_RELAY_HTTP.' },
|
|
63
83
|
relayWs: { type: 'string', description: 'Relay wss URL for NIP-42; takes precedence over RUFLO_X_RELAY_WS.' },
|
|
64
84
|
keyFile: { type: 'string', description: 'Key file path; takes precedence over RUFLO_NOSTR_KEY_FILE (default ~/.ruflo/nostr.key).' }
|
|
65
|
-
}, required: [
|
|
85
|
+
}, required: [] },
|
|
66
86
|
handler: async (input) => {
|
|
67
87
|
const i = input;
|
|
68
88
|
const nt = await loadNostrTools();
|
|
69
89
|
// Validate input before the optional-dependency check so a bad code fails fast and identically
|
|
70
90
|
// whether or not nostr-tools is present.
|
|
71
|
-
if (!/^v2\.[A-Za-z0-9._-]{8,}$/.test(i.code))
|
|
91
|
+
if (i.code !== undefined && !/^v2\.[A-Za-z0-9._-]{8,}$/.test(i.code))
|
|
72
92
|
throw new Error('invite code must look like v2.<token>');
|
|
73
93
|
if (!nt)
|
|
74
94
|
return { degraded: true, reason: 'nostr-tools not installed', hint: 'npm i -g nostr-tools (secp256k1 signing is not in node:crypto)' };
|
|
95
|
+
const registrationBase = i.code ? undefined : secureBase(i.gatewayUrl || process.env.RUFLO_X_GATEWAY_URL || 'https://x.ruv.io');
|
|
75
96
|
const { sk, pubkey, created } = loadOrCreateKey(nt, i.keyFile);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
// Existing members need no new grant. In particular never downgrade/re-admit
|
|
98
|
+
// their key just because they run join again.
|
|
99
|
+
if (!i.code) {
|
|
100
|
+
const existing = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
|
|
101
|
+
if (existing.ok)
|
|
102
|
+
return { ok: true, pubkey, keyCreated: created, membershipVerified: true, alreadyMember: true };
|
|
103
|
+
}
|
|
104
|
+
const url = i.code ? `${secureBase(HTTP_BASE(i.relayHttp))}/api/invites/claim` : `${registrationBase}/api/registration`;
|
|
105
|
+
const body = i.code ? JSON.stringify({ code: i.code }) : '{}';
|
|
106
|
+
let r;
|
|
107
|
+
try {
|
|
108
|
+
r = await fetch(url, { method: 'POST', redirect: 'error', headers: { Authorization: nip98Header(nt, sk, url, 'POST', body), 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(20_000) });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
const check = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
|
|
112
|
+
if (check.ok)
|
|
113
|
+
return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
|
|
114
|
+
throw new Error('Admission response unavailable and relay membership unverified. Keep your local key and retry; do not create another identity.');
|
|
115
|
+
}
|
|
79
116
|
const claim = (await r.json().catch(() => ({})));
|
|
80
|
-
if (!r.ok)
|
|
81
|
-
|
|
117
|
+
if (!r.ok) {
|
|
118
|
+
// A lost acknowledgment can follow a successful grant. Check with the
|
|
119
|
+
// relay before declaring failure; never silently generate a second key.
|
|
120
|
+
const check = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
|
|
121
|
+
if (check.ok)
|
|
122
|
+
return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
|
|
123
|
+
throw new Error(`join rejected (${r.status}): ${claim.error ?? claim.message ?? 'unknown'}`);
|
|
124
|
+
}
|
|
82
125
|
const auth = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
|
|
83
126
|
return { ok: auth.ok, pubkey, keyCreated: created, role: claim.role ?? 'member', membershipVerified: auth.ok, ...(auth.ok ? {} : { reason: auth.reason }),
|
|
84
127
|
next: 'Publish kind-1 events tagged ["t","ruflo-swarm"] — or run `ruflo federation sync` to read the swarm.' };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.47.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|