libp2p-mesh 2026.6.11 → 2026.6.13
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/README.md +124 -0
- package/dist/src/agent-tools.d.ts +120 -1
- package/dist/src/agent-tools.js +153 -0
- package/dist/src/instance-peer-store.js +35 -1
- package/dist/src/instance-router.d.ts +2 -8
- package/dist/src/instance-router.js +94 -2
- package/dist/src/plugin.js +16 -3
- package/dist/src/profile-cli.d.ts +27 -0
- package/dist/src/profile-cli.js +47 -0
- package/dist/src/profile-wizard.d.ts +20 -0
- package/dist/src/profile-wizard.js +141 -0
- package/dist/src/setup-cli.d.ts +19 -0
- package/dist/src/setup-cli.js +32 -28
- package/dist/src/types.d.ts +63 -0
- package/dist/src/user-attributes.d.ts +6 -0
- package/dist/src/user-attributes.js +92 -0
- package/dist/src/user-md-attributes.d.ts +12 -0
- package/dist/src/user-md-attributes.js +202 -0
- package/dist/src/user-profile-store.d.ts +25 -0
- package/dist/src/user-profile-store.js +187 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/src/agent-tools.ts +187 -1
- package/src/instance-peer-store.ts +41 -1
- package/src/instance-router.ts +121 -12
- package/src/plugin.ts +18 -3
- package/src/profile-cli.ts +85 -0
- package/src/profile-wizard.ts +204 -0
- package/src/setup-cli.ts +40 -29
- package/src/types.ts +68 -0
- package/src/user-attributes.ts +122 -0
- package/src/user-md-attributes.ts +256 -0
- package/src/user-profile-store.ts +259 -0
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ P2P mesh network plugin for OpenClaw. Enables direct peer-to-peer communication
|
|
|
10
10
|
- **Bootstrap Mode** — Optional static bootstrap peer list for non-LAN scenarios
|
|
11
11
|
- **WebSocket Transport** — Optional WebSocket support for NAT/firewall-friendly connections
|
|
12
12
|
- **NAT Traversal** — Built-in AutoNAT + UPnP + Circuit Relay v2 + DCUtR for peers behind home routers / firewalls
|
|
13
|
+
- **User Public Attributes** — Announce public tags and structured profile attributes so agents can dry-run and send to locally discovered instances by attribute
|
|
13
14
|
|
|
14
15
|
## Requirements
|
|
15
16
|
|
|
@@ -443,6 +444,129 @@ The sender reports success only after the remote OpenClaw instance forwards the
|
|
|
443
444
|
|
|
444
445
|
Tools are not configured in `openclaw.json`; they are registered automatically by the plugin through `api.registerTool()`.
|
|
445
446
|
|
|
447
|
+
### User public attributes
|
|
448
|
+
|
|
449
|
+
`libp2p-mesh` can announce user public attributes with instance route announcements. These attributes help agents find matching OpenClaw instances after those instances have already been discovered through the mesh.
|
|
450
|
+
|
|
451
|
+
There are two sources:
|
|
452
|
+
|
|
453
|
+
- `USER.md` tags are extracted read-only at gateway startup. The plugin never edits `USER.md`.
|
|
454
|
+
- `user-profile.json` stores manually managed structured attributes such as group, project, role, skill, or a custom key.
|
|
455
|
+
|
|
456
|
+
Run the profile wizard to manage structured attributes:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
openclaw libp2p-mesh profile
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The wizard previews read-only `USER.md` tags and lets you add, edit, or remove only structured profile attributes. Tags extracted from `USER.md` are not written to `user-profile.json`; they are merged in memory with profile attributes and broadcast only in instance announce messages.
|
|
463
|
+
|
|
464
|
+
The default profile path is:
|
|
465
|
+
|
|
466
|
+
```text
|
|
467
|
+
~/.openclaw/libp2p/user-profile.json
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
When `OPENCLAW_STATE_DIR` is set:
|
|
471
|
+
|
|
472
|
+
```text
|
|
473
|
+
$OPENCLAW_STATE_DIR/libp2p/user-profile.json
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Example `user-profile.json`:
|
|
477
|
+
|
|
478
|
+
```json
|
|
479
|
+
{
|
|
480
|
+
"version": 1,
|
|
481
|
+
"updatedAt": 1782180000000,
|
|
482
|
+
"attributes": [
|
|
483
|
+
{
|
|
484
|
+
"kind": "structured",
|
|
485
|
+
"key": "project",
|
|
486
|
+
"value": "openclaw",
|
|
487
|
+
"label": "project: openclaw",
|
|
488
|
+
"source": "profile"
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
"kind": "structured",
|
|
492
|
+
"key": "role",
|
|
493
|
+
"value": "maintainer",
|
|
494
|
+
"label": "role: maintainer",
|
|
495
|
+
"source": "profile"
|
|
496
|
+
}
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Remote attributes are cached in plugin-managed instance state under `instance-peer.json.userPublicAttributes`:
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"version": 1,
|
|
506
|
+
"updatedAt": 1782180000000,
|
|
507
|
+
"instances": {
|
|
508
|
+
"alice-mac@AQIDBAUGBweI.7a3f9e2b": {
|
|
509
|
+
"instanceId": "alice-mac@AQIDBAUGBweI.7a3f9e2b",
|
|
510
|
+
"peerId": "12D3KooW...",
|
|
511
|
+
"instanceName": "alice-mac",
|
|
512
|
+
"multiaddrs": ["/ip4/192.168.1.23/tcp/4001"],
|
|
513
|
+
"userPublicAttributes": [
|
|
514
|
+
{
|
|
515
|
+
"kind": "tag",
|
|
516
|
+
"value": "libp2p",
|
|
517
|
+
"label": "libp2p",
|
|
518
|
+
"source": "USER.md"
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"kind": "structured",
|
|
522
|
+
"key": "project",
|
|
523
|
+
"value": "openclaw",
|
|
524
|
+
"label": "project: openclaw",
|
|
525
|
+
"source": "profile"
|
|
526
|
+
}
|
|
527
|
+
],
|
|
528
|
+
"lastSeenAt": 1782180000000,
|
|
529
|
+
"lastAnnouncedAt": 1782180000000,
|
|
530
|
+
"source": "announce"
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Use `p2p_send_user_attribute_message` for attribute-based group messages. Always dry-run first, review the matched instances with the user, then send only after confirmation:
|
|
537
|
+
|
|
538
|
+
```text
|
|
539
|
+
p2p_send_user_attribute_message({
|
|
540
|
+
"match": { "kind": "structured", "key": "project", "value": "openclaw" },
|
|
541
|
+
"message": "今晚同步一下进展",
|
|
542
|
+
"dryRun": true
|
|
543
|
+
})
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
After confirming the dry-run targets:
|
|
547
|
+
|
|
548
|
+
```text
|
|
549
|
+
p2p_send_user_attribute_message({
|
|
550
|
+
"match": { "kind": "structured", "key": "project", "value": "openclaw" },
|
|
551
|
+
"message": "今晚同步一下进展",
|
|
552
|
+
"dryRun": false
|
|
553
|
+
})
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Tag matches use only the tag value:
|
|
557
|
+
|
|
558
|
+
```text
|
|
559
|
+
p2p_send_user_attribute_message({
|
|
560
|
+
"match": { "kind": "tag", "value": "libp2p" },
|
|
561
|
+
"message": "libp2p 方向有个问题想确认",
|
|
562
|
+
"dryRun": true
|
|
563
|
+
})
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
The first version matches only instances already present in the local `instance-peer.json` discovery cache. It does not search the whole network or ask disconnected peers for more users.
|
|
567
|
+
|
|
568
|
+
Privacy boundary: public attributes are broadcast with instance announce messages to peers your gateway connects to. Do not put private, sensitive, or access-controlled information in `USER.md` tags or `user-profile.json` structured attributes.
|
|
569
|
+
|
|
446
570
|
## Troubleshooting
|
|
447
571
|
|
|
448
572
|
### Peers do not discover each other
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type { DeliveryTargetResult, InstanceRouter, MeshNetwork } from "./types.js";
|
|
1
|
+
import type { DeliveryTargetResult, InstanceRouter, MeshNetwork, UserPublicAttribute } from "./types.js";
|
|
2
|
+
type SendUserAttributeToolParams = {
|
|
3
|
+
match?: {
|
|
4
|
+
kind?: unknown;
|
|
5
|
+
key?: unknown;
|
|
6
|
+
value?: unknown;
|
|
7
|
+
};
|
|
8
|
+
message?: unknown;
|
|
9
|
+
dryRun?: unknown;
|
|
10
|
+
};
|
|
2
11
|
export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter): ({
|
|
3
12
|
name: string;
|
|
4
13
|
label: string;
|
|
@@ -16,6 +25,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
16
25
|
};
|
|
17
26
|
topic?: undefined;
|
|
18
27
|
instanceId?: undefined;
|
|
28
|
+
match?: undefined;
|
|
29
|
+
dryRun?: undefined;
|
|
19
30
|
};
|
|
20
31
|
required: string[];
|
|
21
32
|
};
|
|
@@ -62,6 +73,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
62
73
|
};
|
|
63
74
|
peerId?: undefined;
|
|
64
75
|
instanceId?: undefined;
|
|
76
|
+
match?: undefined;
|
|
77
|
+
dryRun?: undefined;
|
|
65
78
|
};
|
|
66
79
|
required: string[];
|
|
67
80
|
};
|
|
@@ -102,6 +115,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
102
115
|
message?: undefined;
|
|
103
116
|
topic?: undefined;
|
|
104
117
|
instanceId?: undefined;
|
|
118
|
+
match?: undefined;
|
|
119
|
+
dryRun?: undefined;
|
|
105
120
|
};
|
|
106
121
|
required?: undefined;
|
|
107
122
|
};
|
|
@@ -141,6 +156,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
141
156
|
message?: undefined;
|
|
142
157
|
topic?: undefined;
|
|
143
158
|
instanceId?: undefined;
|
|
159
|
+
match?: undefined;
|
|
160
|
+
dryRun?: undefined;
|
|
144
161
|
};
|
|
145
162
|
required?: undefined;
|
|
146
163
|
};
|
|
@@ -189,6 +206,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
189
206
|
message?: undefined;
|
|
190
207
|
topic?: undefined;
|
|
191
208
|
instanceId?: undefined;
|
|
209
|
+
match?: undefined;
|
|
210
|
+
dryRun?: undefined;
|
|
192
211
|
};
|
|
193
212
|
required?: undefined;
|
|
194
213
|
};
|
|
@@ -230,6 +249,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
230
249
|
message?: undefined;
|
|
231
250
|
topic?: undefined;
|
|
232
251
|
instanceId?: undefined;
|
|
252
|
+
match?: undefined;
|
|
253
|
+
dryRun?: undefined;
|
|
233
254
|
};
|
|
234
255
|
required?: undefined;
|
|
235
256
|
};
|
|
@@ -258,6 +279,7 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
258
279
|
instanceName?: string;
|
|
259
280
|
multiaddrs: string[];
|
|
260
281
|
pubkey?: string;
|
|
282
|
+
userPublicAttributes?: UserPublicAttribute[];
|
|
261
283
|
lastSeenAt: number;
|
|
262
284
|
lastAnnouncedAt: number;
|
|
263
285
|
source: "announce";
|
|
@@ -294,6 +316,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
294
316
|
peerId?: undefined;
|
|
295
317
|
message?: undefined;
|
|
296
318
|
topic?: undefined;
|
|
319
|
+
match?: undefined;
|
|
320
|
+
dryRun?: undefined;
|
|
297
321
|
};
|
|
298
322
|
required: string[];
|
|
299
323
|
};
|
|
@@ -369,6 +393,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
369
393
|
};
|
|
370
394
|
peerId?: undefined;
|
|
371
395
|
topic?: undefined;
|
|
396
|
+
match?: undefined;
|
|
397
|
+
dryRun?: undefined;
|
|
372
398
|
};
|
|
373
399
|
required: string[];
|
|
374
400
|
};
|
|
@@ -430,4 +456,97 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
430
456
|
};
|
|
431
457
|
isError?: undefined;
|
|
432
458
|
}>;
|
|
459
|
+
} | {
|
|
460
|
+
name: string;
|
|
461
|
+
label: string;
|
|
462
|
+
description: string;
|
|
463
|
+
parameters: {
|
|
464
|
+
type: "object";
|
|
465
|
+
properties: {
|
|
466
|
+
match: {
|
|
467
|
+
type: "object";
|
|
468
|
+
description: string;
|
|
469
|
+
oneOf: ({
|
|
470
|
+
type: "object";
|
|
471
|
+
additionalProperties: boolean;
|
|
472
|
+
properties: {
|
|
473
|
+
kind: {
|
|
474
|
+
const: "tag";
|
|
475
|
+
};
|
|
476
|
+
value: {
|
|
477
|
+
type: "string";
|
|
478
|
+
description: string;
|
|
479
|
+
};
|
|
480
|
+
key?: undefined;
|
|
481
|
+
};
|
|
482
|
+
required: string[];
|
|
483
|
+
} | {
|
|
484
|
+
type: "object";
|
|
485
|
+
additionalProperties: boolean;
|
|
486
|
+
properties: {
|
|
487
|
+
kind: {
|
|
488
|
+
const: "structured";
|
|
489
|
+
};
|
|
490
|
+
key: {
|
|
491
|
+
type: "string";
|
|
492
|
+
description: string;
|
|
493
|
+
};
|
|
494
|
+
value: {
|
|
495
|
+
type: "string";
|
|
496
|
+
description: string;
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
required: string[];
|
|
500
|
+
})[];
|
|
501
|
+
};
|
|
502
|
+
message: {
|
|
503
|
+
type: "string";
|
|
504
|
+
description: string;
|
|
505
|
+
};
|
|
506
|
+
dryRun: {
|
|
507
|
+
type: "boolean";
|
|
508
|
+
description: string;
|
|
509
|
+
};
|
|
510
|
+
peerId?: undefined;
|
|
511
|
+
topic?: undefined;
|
|
512
|
+
instanceId?: undefined;
|
|
513
|
+
};
|
|
514
|
+
required: string[];
|
|
515
|
+
};
|
|
516
|
+
execute(_toolCallId: string, params: SendUserAttributeToolParams): Promise<{
|
|
517
|
+
content: {
|
|
518
|
+
type: "text";
|
|
519
|
+
text: string;
|
|
520
|
+
}[];
|
|
521
|
+
details: {
|
|
522
|
+
initialized: boolean;
|
|
523
|
+
error?: undefined;
|
|
524
|
+
};
|
|
525
|
+
isError: boolean;
|
|
526
|
+
} | {
|
|
527
|
+
content: {
|
|
528
|
+
type: "text";
|
|
529
|
+
text: string;
|
|
530
|
+
}[];
|
|
531
|
+
details: {
|
|
532
|
+
error: string;
|
|
533
|
+
initialized?: undefined;
|
|
534
|
+
};
|
|
535
|
+
isError: boolean;
|
|
536
|
+
} | {
|
|
537
|
+
content: {
|
|
538
|
+
type: "text";
|
|
539
|
+
text: string;
|
|
540
|
+
}[];
|
|
541
|
+
details: import("./types.js").UserAttributeMessageResult;
|
|
542
|
+
isError?: undefined;
|
|
543
|
+
} | {
|
|
544
|
+
content: {
|
|
545
|
+
type: "text";
|
|
546
|
+
text: string;
|
|
547
|
+
}[];
|
|
548
|
+
details: import("./types.js").UserAttributeMessageResult;
|
|
549
|
+
isError: boolean | undefined;
|
|
550
|
+
}>;
|
|
433
551
|
})[];
|
|
552
|
+
export {};
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -12,6 +12,49 @@ function formatDeliveryResults(instanceId, delivered, results) {
|
|
|
12
12
|
});
|
|
13
13
|
return [heading, ...lines].join("\n");
|
|
14
14
|
}
|
|
15
|
+
function attributeLabel(attribute) {
|
|
16
|
+
if (attribute.kind === "tag") {
|
|
17
|
+
return `tag:${attribute.value}`;
|
|
18
|
+
}
|
|
19
|
+
return `${attribute.key}:${attribute.value}`;
|
|
20
|
+
}
|
|
21
|
+
function instanceTargetLabel(target) {
|
|
22
|
+
const name = target.instanceName ? ` (${target.instanceName})` : "";
|
|
23
|
+
return `${target.instanceId}${name} -> ${target.peerId}`;
|
|
24
|
+
}
|
|
25
|
+
function formatUserAttributeTargets(targets) {
|
|
26
|
+
return targets.map((target) => `${instanceTargetLabel(target)} [${attributeLabel(target.matchedAttribute)}]`);
|
|
27
|
+
}
|
|
28
|
+
function formatUserAttributeResults(results) {
|
|
29
|
+
return results.map((result) => {
|
|
30
|
+
let status = "已送达";
|
|
31
|
+
if (!result.sent) {
|
|
32
|
+
status = `发送失败:${result.error ?? "unknown error"}`;
|
|
33
|
+
}
|
|
34
|
+
else if (!result.delivered) {
|
|
35
|
+
status = `投递失败:${result.error ?? "unknown error"}`;
|
|
36
|
+
}
|
|
37
|
+
return `${instanceTargetLabel(result)}:${status}`;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function normalizeUserAttributeMatch(params) {
|
|
41
|
+
const match = params.match;
|
|
42
|
+
if (!match || typeof match !== "object") {
|
|
43
|
+
return "match is required.";
|
|
44
|
+
}
|
|
45
|
+
if (match.kind === "tag") {
|
|
46
|
+
const value = typeof match.value === "string" ? match.value.trim() : "";
|
|
47
|
+
return value ? { kind: "tag", value } : "match.value is required for tag matches.";
|
|
48
|
+
}
|
|
49
|
+
if (match.kind === "structured") {
|
|
50
|
+
const key = typeof match.key === "string" ? match.key.trim() : "";
|
|
51
|
+
const value = typeof match.value === "string" ? match.value.trim() : "";
|
|
52
|
+
return key && value
|
|
53
|
+
? { kind: "structured", key, value }
|
|
54
|
+
: "match.key and match.value are required for structured matches.";
|
|
55
|
+
}
|
|
56
|
+
return 'match.kind must be "tag" or "structured".';
|
|
57
|
+
}
|
|
15
58
|
export function buildP2PTools(mesh, router) {
|
|
16
59
|
return [
|
|
17
60
|
{
|
|
@@ -369,5 +412,115 @@ export function buildP2PTools(mesh, router) {
|
|
|
369
412
|
};
|
|
370
413
|
},
|
|
371
414
|
},
|
|
415
|
+
{
|
|
416
|
+
name: "p2p_send_user_attribute_message",
|
|
417
|
+
label: "P2P Send User Attribute Message",
|
|
418
|
+
description: "Send a user message to discovered OpenClaw instances matching a public user attribute. Always run a dry run with dryRun=true first and ask the user to confirm targets before group sending.",
|
|
419
|
+
parameters: {
|
|
420
|
+
type: "object",
|
|
421
|
+
properties: {
|
|
422
|
+
match: {
|
|
423
|
+
type: "object",
|
|
424
|
+
description: 'User public attribute match. Use { "kind": "tag", "value": "..." } for USER.md tags or { "kind": "structured", "key": "...", "value": "..." } for profile attributes.',
|
|
425
|
+
oneOf: [
|
|
426
|
+
{
|
|
427
|
+
type: "object",
|
|
428
|
+
additionalProperties: false,
|
|
429
|
+
properties: {
|
|
430
|
+
kind: { const: "tag" },
|
|
431
|
+
value: {
|
|
432
|
+
type: "string",
|
|
433
|
+
description: "Tag value to match.",
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
required: ["kind", "value"],
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
type: "object",
|
|
440
|
+
additionalProperties: false,
|
|
441
|
+
properties: {
|
|
442
|
+
kind: { const: "structured" },
|
|
443
|
+
key: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: "Structured attribute key to match.",
|
|
446
|
+
},
|
|
447
|
+
value: {
|
|
448
|
+
type: "string",
|
|
449
|
+
description: "Structured attribute value to match.",
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
required: ["kind", "key", "value"],
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
message: {
|
|
457
|
+
type: "string",
|
|
458
|
+
description: "Message content to send after dry-run target confirmation.",
|
|
459
|
+
},
|
|
460
|
+
dryRun: {
|
|
461
|
+
type: "boolean",
|
|
462
|
+
description: "Preview matching instances without sending. Run this before group sending.",
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
required: ["match", "message"],
|
|
466
|
+
},
|
|
467
|
+
async execute(_toolCallId, params) {
|
|
468
|
+
if (!router) {
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text: "Instance router is not initialized." }],
|
|
471
|
+
details: { initialized: false },
|
|
472
|
+
isError: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const match = normalizeUserAttributeMatch(params);
|
|
476
|
+
const message = typeof params.message === "string" ? params.message.trim() : "";
|
|
477
|
+
if (typeof match === "string" || !message) {
|
|
478
|
+
const error = typeof match === "string" ? match : "message is required.";
|
|
479
|
+
return {
|
|
480
|
+
content: [{ type: "text", text: error }],
|
|
481
|
+
details: { error },
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const dryRun = params.dryRun === true;
|
|
486
|
+
const result = await router.sendUserAttributeMessage(match, message, { dryRun });
|
|
487
|
+
if (result.error) {
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: result.error }],
|
|
490
|
+
details: result,
|
|
491
|
+
isError: true,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (dryRun) {
|
|
495
|
+
const targetLines = formatUserAttributeTargets(result.targets ?? []);
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: [
|
|
501
|
+
`Dry run matched ${result.matched} instance(s). No message was sent.`,
|
|
502
|
+
...targetLines,
|
|
503
|
+
].join("\n"),
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
details: result,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const resultLines = formatUserAttributeResults(result.results ?? []);
|
|
510
|
+
return {
|
|
511
|
+
content: [
|
|
512
|
+
{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: [
|
|
515
|
+
`Matched ${result.matched} instance(s); sent ${result.sent}; delivered ${result.delivered}; failed ${result.failed}.`,
|
|
516
|
+
...resultLines,
|
|
517
|
+
].join("\n"),
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
details: result,
|
|
521
|
+
isError: result.failed > 0 ? true : undefined,
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
},
|
|
372
525
|
];
|
|
373
526
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { normalizeUserPublicAttribute } from "./user-attributes.js";
|
|
4
5
|
export function resolveInstancePeerPath(customPath) {
|
|
5
6
|
if (customPath)
|
|
6
7
|
return customPath;
|
|
@@ -22,25 +23,56 @@ function sameStringArray(a = [], b = []) {
|
|
|
22
23
|
return false;
|
|
23
24
|
return a.every((value, index) => value === b[index]);
|
|
24
25
|
}
|
|
26
|
+
function normalizeUserPublicAttributes(value) {
|
|
27
|
+
if (!Array.isArray(value)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return value
|
|
31
|
+
.map((attribute) => normalizeUserPublicAttribute(attribute))
|
|
32
|
+
.filter((attribute) => attribute !== undefined);
|
|
33
|
+
}
|
|
34
|
+
function sameUserPublicAttributes(a = [], b = []) {
|
|
35
|
+
if (a.length !== b.length)
|
|
36
|
+
return false;
|
|
37
|
+
return a.every((attribute, index) => {
|
|
38
|
+
const other = b[index];
|
|
39
|
+
return JSON.stringify(attribute) === JSON.stringify(other);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
25
42
|
function sameRecord(record, payload) {
|
|
26
43
|
if (!record)
|
|
27
44
|
return false;
|
|
45
|
+
const recordAttributes = normalizeUserPublicAttributes(record.userPublicAttributes);
|
|
46
|
+
const payloadAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
|
|
28
47
|
return (record.peerId === payload.peerId &&
|
|
29
48
|
record.instanceName === payload.instanceName &&
|
|
30
49
|
record.pubkey === payload.pubkey &&
|
|
31
50
|
sameStringArray(record.multiaddrs, payload.multiaddrs) &&
|
|
51
|
+
sameUserPublicAttributes(recordAttributes, payloadAttributes) &&
|
|
32
52
|
record.lastAnnouncedAt === payload.announcedAt);
|
|
33
53
|
}
|
|
54
|
+
function normalizeRecord(value) {
|
|
55
|
+
return {
|
|
56
|
+
...value,
|
|
57
|
+
userPublicAttributes: normalizeUserPublicAttributes(value.userPublicAttributes),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
34
60
|
function normalizeTable(value) {
|
|
35
61
|
const candidate = value && typeof value === "object" ? value : {};
|
|
36
62
|
const table = candidate;
|
|
37
63
|
const instances = table.instances && typeof table.instances === "object" && !Array.isArray(table.instances)
|
|
38
64
|
? table.instances
|
|
39
65
|
: {};
|
|
66
|
+
const normalizedInstances = {};
|
|
67
|
+
for (const [instanceId, record] of Object.entries(instances)) {
|
|
68
|
+
if (record && typeof record === "object") {
|
|
69
|
+
normalizedInstances[instanceId] = normalizeRecord(record);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
40
72
|
return {
|
|
41
73
|
version: 1,
|
|
42
74
|
updatedAt: typeof table.updatedAt === "number" ? table.updatedAt : Date.now(),
|
|
43
|
-
instances:
|
|
75
|
+
instances: normalizedInstances,
|
|
44
76
|
};
|
|
45
77
|
}
|
|
46
78
|
export function createInstancePeerStore(options) {
|
|
@@ -103,6 +135,7 @@ export function createInstancePeerStore(options) {
|
|
|
103
135
|
return runMutation(async () => {
|
|
104
136
|
const table = await load();
|
|
105
137
|
const existing = table.instances[payload.instanceId];
|
|
138
|
+
const userPublicAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
|
|
106
139
|
const changed = !sameRecord(existing, payload);
|
|
107
140
|
const record = {
|
|
108
141
|
instanceId: payload.instanceId,
|
|
@@ -110,6 +143,7 @@ export function createInstancePeerStore(options) {
|
|
|
110
143
|
instanceName: payload.instanceName,
|
|
111
144
|
pubkey: payload.pubkey,
|
|
112
145
|
multiaddrs: payload.multiaddrs,
|
|
146
|
+
userPublicAttributes,
|
|
113
147
|
lastAnnouncedAt: payload.announcedAt,
|
|
114
148
|
lastSeenAt: Date.now(),
|
|
115
149
|
source: "announce",
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { InstanceRouter, InstanceRouterOptions } from "./types.js";
|
|
2
2
|
export type RouterLogger = {
|
|
3
3
|
info?: (message: string) => void;
|
|
4
4
|
debug?: (message: string) => void;
|
|
5
5
|
warn?: (message: string) => void;
|
|
6
6
|
error?: (message: string) => void;
|
|
7
7
|
};
|
|
8
|
-
export declare function createInstanceRouter(options:
|
|
9
|
-
mesh: MeshNetwork;
|
|
10
|
-
store: InstancePeerStore;
|
|
11
|
-
delivery: InboundDeliveryAdapter;
|
|
12
|
-
config?: MeshConfig;
|
|
13
|
-
logger?: RouterLogger;
|
|
14
|
-
}): InstanceRouter;
|
|
8
|
+
export declare function createInstanceRouter(options: InstanceRouterOptions): InstanceRouter;
|