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.
@@ -1,3 +1,4 @@
1
+ import { matchesUserAttribute, mergeUserPublicAttributes } from "./user-attributes.js";
1
2
  const MAX_DELIVERY_CACHE_ENTRIES = 1000;
2
3
  function parsePayload(msg) {
3
4
  try {
@@ -13,6 +14,12 @@ function isNonEmptyString(value) {
13
14
  function summarizeError(error) {
14
15
  return error instanceof Error ? error.message : String(error);
15
16
  }
17
+ function describeAttributeMatch(match) {
18
+ if (match.kind === "tag") {
19
+ return `tag ${match.value}`;
20
+ }
21
+ return `structured attribute ${match.key}=${match.value}`;
22
+ }
16
23
  function displayTargetId(target) {
17
24
  const id = typeof target.id === "string" ? target.id.trim() : "";
18
25
  return id && id.length > 0 ? id : undefined;
@@ -86,7 +93,20 @@ export function createInstanceRouter(options) {
86
93
  }
87
94
  return identity.id;
88
95
  }
89
- function buildAnnouncePayload() {
96
+ async function loadUserPublicAttributes() {
97
+ const [userMdTags, profileAttributes] = await Promise.all([
98
+ options.userAttributeSource?.loadTags().catch((error) => {
99
+ logger?.warn?.(`[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`);
100
+ return [];
101
+ }) ?? Promise.resolve([]),
102
+ options.userProfileStore?.listAttributes().catch((error) => {
103
+ logger?.warn?.(`[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`);
104
+ return [];
105
+ }) ?? Promise.resolve([]),
106
+ ]);
107
+ return mergeUserPublicAttributes(userMdTags, profileAttributes);
108
+ }
109
+ async function buildAnnouncePayload() {
90
110
  const identity = mesh.getInstanceIdentity();
91
111
  if (!identity) {
92
112
  throw new Error("Local instance identity is not initialized");
@@ -97,6 +117,7 @@ export function createInstanceRouter(options) {
97
117
  instanceName: identity.name,
98
118
  multiaddrs: mesh.getMultiaddrs(),
99
119
  pubkey: identity.pubkey,
120
+ userPublicAttributes: await loadUserPublicAttributes(),
100
121
  announcedAt: Date.now(),
101
122
  };
102
123
  }
@@ -104,7 +125,7 @@ export function createInstanceRouter(options) {
104
125
  if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
105
126
  return;
106
127
  }
107
- const payload = buildAnnouncePayload();
128
+ const payload = await buildAnnouncePayload();
108
129
  await mesh.sendStructuredMessage(peerId, {
109
130
  id: crypto.randomUUID(),
110
131
  type: "instance-announce",
@@ -413,6 +434,76 @@ export function createInstanceRouter(options) {
413
434
  error: ack.error,
414
435
  };
415
436
  }
437
+ async function resolveUserAttributeTargets(match) {
438
+ const records = await store.list();
439
+ const targets = [];
440
+ for (const record of records) {
441
+ const matchedAttribute = record.userPublicAttributes?.find((attribute) => matchesUserAttribute(attribute, match));
442
+ if (!matchedAttribute) {
443
+ continue;
444
+ }
445
+ targets.push({
446
+ instanceId: record.instanceId,
447
+ instanceName: record.instanceName,
448
+ peerId: record.peerId,
449
+ matchedAttribute,
450
+ });
451
+ }
452
+ return targets;
453
+ }
454
+ async function sendUserAttributeMessage(match, message, sendOptions = {}) {
455
+ const targets = await resolveUserAttributeTargets(match);
456
+ if (targets.length === 0) {
457
+ return {
458
+ matched: 0,
459
+ sent: 0,
460
+ delivered: 0,
461
+ failed: 0,
462
+ error: `No discovered instances match ${describeAttributeMatch(match)}.`,
463
+ };
464
+ }
465
+ if (sendOptions.dryRun === true) {
466
+ return {
467
+ matched: targets.length,
468
+ sent: 0,
469
+ delivered: 0,
470
+ failed: 0,
471
+ targets,
472
+ };
473
+ }
474
+ const results = [];
475
+ for (const target of targets) {
476
+ try {
477
+ const result = await sendInstanceMessage(target.instanceId, message);
478
+ const targetResult = {
479
+ ...target,
480
+ sent: result.sent,
481
+ delivered: result.delivered,
482
+ };
483
+ if (result.error) {
484
+ results.push({ ...targetResult, error: result.error });
485
+ }
486
+ else {
487
+ results.push(targetResult);
488
+ }
489
+ }
490
+ catch (error) {
491
+ results.push({
492
+ ...target,
493
+ sent: false,
494
+ delivered: false,
495
+ error: summarizeError(error),
496
+ });
497
+ }
498
+ }
499
+ return {
500
+ matched: targets.length,
501
+ sent: results.filter((result) => result.sent).length,
502
+ delivered: results.filter((result) => result.delivered).length,
503
+ failed: results.filter((result) => !result.delivered).length,
504
+ results,
505
+ };
506
+ }
416
507
  return {
417
508
  start,
418
509
  stop,
@@ -421,5 +512,6 @@ export function createInstanceRouter(options) {
421
512
  listInstances,
422
513
  resolveInstance,
423
514
  sendInstanceMessage,
515
+ sendUserAttributeMessage,
424
516
  };
425
517
  }
@@ -4,10 +4,12 @@ import { createOpenClawRuntimeInboundDelivery } from "./inbound-delivery.js";
4
4
  import { createInstancePeerStore } from "./instance-peer-store.js";
5
5
  import { createInstanceRouter } from "./instance-router.js";
6
6
  import { createMeshNetwork } from "./mesh.js";
7
+ import { createUserMdAttributeSource } from "./user-md-attributes.js";
8
+ import { createUserProfileStore } from "./user-profile-store.js";
7
9
  import { buildP2PTools } from "./agent-tools.js";
8
- import { registerLibp2pMeshSetupCli } from "./setup-cli.js";
10
+ import { registerLibp2pMeshCli } from "./profile-cli.js";
9
11
  export function registerLibp2pMesh(api) {
10
- registerLibp2pMeshSetupCli(api);
12
+ registerLibp2pMeshCli(api);
11
13
  const config = api.pluginConfig;
12
14
  let unsubscribeInbound;
13
15
  let serviceStarted = false;
@@ -16,9 +18,18 @@ export function registerLibp2pMesh(api) {
16
18
  logger: api.logger,
17
19
  });
18
20
  const store = createInstancePeerStore({ logger: api.logger });
21
+ const userAttributeSource = createUserMdAttributeSource({ logger: api.logger });
22
+ const userProfileStore = createUserProfileStore({ logger: api.logger });
19
23
  const delivery = createOpenClawRuntimeInboundDelivery({
20
24
  config: api.config,
21
- loadAdapter: api.runtime.channel.outbound.loadAdapter,
25
+ loadAdapter: async (channelId) => {
26
+ const loadAdapter = api.runtime.channel?.outbound?.loadAdapter;
27
+ if (!loadAdapter) {
28
+ api.logger.warn?.("[libp2p-mesh] Runtime channel outbound adapter is unavailable; inbound delivery is disabled in this context.");
29
+ return undefined;
30
+ }
31
+ return loadAdapter(channelId);
32
+ },
22
33
  logger: api.logger,
23
34
  });
24
35
  const router = createInstanceRouter({
@@ -27,6 +38,8 @@ export function registerLibp2pMesh(api) {
27
38
  delivery,
28
39
  config,
29
40
  logger: api.logger,
41
+ userAttributeSource,
42
+ userProfileStore,
30
43
  });
31
44
  const channel = createLibp2pMeshChannel(mesh);
32
45
  // 1. Register Service (manages libp2p node lifecycle)
@@ -0,0 +1,27 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
+ import { type SetupCliDeps } from "./setup-cli.js";
4
+ import { type UserProfileStore } from "./user-profile-store.js";
5
+ import type { SetupPrompter } from "./setup-wizard.js";
6
+ type CliRootCommand = {
7
+ command(name: string): {
8
+ description(text: string): {
9
+ action(handler: () => Promise<void>): void;
10
+ };
11
+ };
12
+ };
13
+ export type ProfileCliDeps = {
14
+ createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
15
+ createProfileStore?: (api: OpenClawPluginApi) => Pick<UserProfileStore, "listAttributes" | "replaceAttributes">;
16
+ createUserMdAttributeSource?: (api: OpenClawPluginApi) => {
17
+ loadTags(): Promise<Awaited<ReturnType<UserProfileStore["listAttributes"]>>>;
18
+ };
19
+ };
20
+ export type Libp2pMeshCliDeps = {
21
+ setup?: SetupCliDeps;
22
+ profile?: ProfileCliDeps;
23
+ };
24
+ export declare function registerLibp2pMeshCli(api: OpenClawPluginApi, deps?: Libp2pMeshCliDeps): void;
25
+ export declare function registerLibp2pMeshProfileCli(api: OpenClawPluginApi, deps?: ProfileCliDeps): void;
26
+ export declare function registerLibp2pMeshProfileCommand(root: CliRootCommand, api: OpenClawPluginApi, ctx: OpenClawPluginCliContext, deps?: ProfileCliDeps): void;
27
+ export {};
@@ -0,0 +1,47 @@
1
+ import { createReadlinePrompter, LIBP2P_MESH_CLI_REGISTRATION, registerLibp2pMeshSetupCommand, } from "./setup-cli.js";
2
+ import { runProfileWizard } from "./profile-wizard.js";
3
+ import { createUserMdAttributeSource } from "./user-md-attributes.js";
4
+ import { createUserProfileStore } from "./user-profile-store.js";
5
+ export function registerLibp2pMeshCli(api, deps = {}) {
6
+ api.registerCli((ctx) => {
7
+ const root = ctx.program
8
+ .command("libp2p-mesh")
9
+ .description("Configure libp2p-mesh plugin.");
10
+ registerLibp2pMeshSetupCommand(root, api, ctx, deps.setup);
11
+ registerLibp2pMeshProfileCommand(root, api, ctx, deps.profile);
12
+ }, LIBP2P_MESH_CLI_REGISTRATION);
13
+ }
14
+ export function registerLibp2pMeshProfileCli(api, deps = {}) {
15
+ api.registerCli((ctx) => {
16
+ const root = ctx.program
17
+ .command("libp2p-mesh")
18
+ .description("Configure libp2p-mesh plugin.");
19
+ registerLibp2pMeshProfileCommand(root, api, ctx, deps);
20
+ }, LIBP2P_MESH_CLI_REGISTRATION);
21
+ }
22
+ export function registerLibp2pMeshProfileCommand(root, api, ctx, deps = {}) {
23
+ root
24
+ .command("profile")
25
+ .description("Manage libp2p-mesh public profile attributes.")
26
+ .action(async () => {
27
+ const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter());
28
+ const profileStore = deps.createProfileStore?.(api) ?? createUserProfileStore({ logger: api.logger });
29
+ const userMdAttributeSource = deps.createUserMdAttributeSource?.(api) ?? createUserMdAttributeSource({ logger: api.logger });
30
+ try {
31
+ const result = await runProfileWizard({
32
+ prompter,
33
+ readOnlyTags: await userMdAttributeSource.loadTags(),
34
+ profileAttributes: await profileStore.listAttributes(),
35
+ writer: {
36
+ async replaceAttributes(attributes) {
37
+ await profileStore.replaceAttributes(attributes);
38
+ },
39
+ },
40
+ });
41
+ prompter.print(result.message);
42
+ }
43
+ finally {
44
+ prompter.close?.();
45
+ }
46
+ });
47
+ }
@@ -0,0 +1,20 @@
1
+ import { type SetupPrompter } from "./setup-wizard.js";
2
+ import type { UserPublicAttribute } from "./types.js";
3
+ export type UserProfileWriter = {
4
+ replaceAttributes(attributes: UserPublicAttribute[]): Promise<void>;
5
+ };
6
+ export type RunProfileWizardOptions = {
7
+ prompter: SetupPrompter;
8
+ readOnlyTags: UserPublicAttribute[];
9
+ profileAttributes: UserPublicAttribute[];
10
+ writer: UserProfileWriter;
11
+ };
12
+ export type ProfileWizardResult = {
13
+ status: "saved";
14
+ attributes: UserPublicAttribute[];
15
+ message: string;
16
+ } | {
17
+ status: "cancelled";
18
+ message: string;
19
+ };
20
+ export declare function runProfileWizard(options: RunProfileWizardOptions): Promise<ProfileWizardResult>;
@@ -0,0 +1,141 @@
1
+ import { SetupCancelledError } from "./setup-wizard.js";
2
+ import { mergeUserPublicAttributes, normalizeAttributeKey, normalizeUserPublicAttribute, } from "./user-attributes.js";
3
+ const CANCELLED_MESSAGE = "Profile update cancelled. No changes were written.";
4
+ const SAVED_MESSAGE = "Profile attributes saved.\n\nRestart the gateway to broadcast updated attributes.";
5
+ export async function runProfileWizard(options) {
6
+ try {
7
+ const readOnlyTags = options.readOnlyTags
8
+ .map((attribute) => normalizeUserPublicAttribute(attribute))
9
+ .filter((attribute) => attribute?.kind === "tag");
10
+ let attributes = normalizeProfileAttributes(options.profileAttributes);
11
+ options.prompter.print(formatProfileOverview(readOnlyTags, attributes));
12
+ while (true) {
13
+ const action = await options.prompter.select("What do you want to do?", [
14
+ { label: "Add structured attribute", value: "add-attribute" },
15
+ { label: "Edit structured attribute", value: "edit-attribute" },
16
+ { label: "Remove structured attribute", value: "remove-attribute" },
17
+ { label: "Preview and finish", value: "preview-finish" },
18
+ { label: "Cancel", value: "cancel" },
19
+ ]);
20
+ switch (action) {
21
+ case "add-attribute":
22
+ attributes = mergeUserPublicAttributes([], [...attributes, await promptForAttribute(options.prompter)]);
23
+ options.prompter.print(formatProfileOverview(readOnlyTags, attributes));
24
+ break;
25
+ case "edit-attribute":
26
+ attributes = await promptForAttributeEdit(options.prompter, attributes);
27
+ options.prompter.print(formatProfileOverview(readOnlyTags, attributes));
28
+ break;
29
+ case "remove-attribute":
30
+ attributes = await promptForAttributeRemoval(options.prompter, attributes);
31
+ options.prompter.print(formatProfileOverview(readOnlyTags, attributes));
32
+ break;
33
+ case "preview-finish":
34
+ options.prompter.print(formatProfilePreview(readOnlyTags, attributes));
35
+ if (!(await options.prompter.confirm("Save profile attributes?", true))) {
36
+ return cancelledResult();
37
+ }
38
+ await options.writer.replaceAttributes(attributes);
39
+ return {
40
+ status: "saved",
41
+ attributes,
42
+ message: SAVED_MESSAGE,
43
+ };
44
+ case "cancel":
45
+ return cancelledResult();
46
+ }
47
+ }
48
+ }
49
+ catch (error) {
50
+ if (error instanceof SetupCancelledError) {
51
+ return cancelledResult();
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+ function normalizeProfileAttributes(attributes) {
57
+ return mergeUserPublicAttributes([], attributes).filter((attribute) => attribute.kind === "structured");
58
+ }
59
+ async function promptForAttribute(prompter) {
60
+ const category = await prompter.select("Attribute category", [
61
+ { label: "Group", value: "group" },
62
+ { label: "Project", value: "project" },
63
+ { label: "Role", value: "role" },
64
+ { label: "Skill", value: "skill" },
65
+ { label: "Custom key", value: "custom" },
66
+ ]);
67
+ const key = category === "custom"
68
+ ? normalizeAttributeKey(await prompter.input("Custom key", { required: true }))
69
+ : category;
70
+ const value = await prompter.input("Attribute value", { required: true });
71
+ return {
72
+ kind: "structured",
73
+ key,
74
+ value: value.trim(),
75
+ label: `${key}: ${value.trim()}`,
76
+ source: "profile",
77
+ };
78
+ }
79
+ async function promptForAttributeEdit(prompter, attributes) {
80
+ if (attributes.length === 0) {
81
+ prompter.print("No structured profile attributes configured.");
82
+ return attributes;
83
+ }
84
+ const selectedIndex = await selectAttributeIndex(prompter, "Attribute to edit", attributes);
85
+ const nextAttribute = await promptForAttribute(prompter);
86
+ return mergeUserPublicAttributes([], attributes.map((attribute, index) => (index === selectedIndex ? nextAttribute : attribute))).filter((attribute) => attribute.kind === "structured");
87
+ }
88
+ async function promptForAttributeRemoval(prompter, attributes) {
89
+ if (attributes.length === 0) {
90
+ prompter.print("No structured profile attributes configured.");
91
+ return attributes;
92
+ }
93
+ const selectedIndex = await selectAttributeIndex(prompter, "Attribute to remove", attributes);
94
+ return attributes.filter((_attribute, index) => index !== selectedIndex);
95
+ }
96
+ async function selectAttributeIndex(prompter, message, attributes) {
97
+ const selectedKey = await prompter.select(message, attributes.map((attribute, index) => ({
98
+ label: formatAttribute(attribute),
99
+ value: `attribute-index-${index}`,
100
+ })));
101
+ const match = /^attribute-index-(\d+)$/.exec(selectedKey);
102
+ return match ? Number(match[1]) : -1;
103
+ }
104
+ function formatProfileOverview(readOnlyTags, attributes) {
105
+ return [
106
+ "Read-only USER.md tags:",
107
+ ...formatAttributeList(readOnlyTags),
108
+ "",
109
+ "Structured profile attributes:",
110
+ ...formatAttributeList(attributes),
111
+ ].join("\n");
112
+ }
113
+ function formatProfilePreview(readOnlyTags, attributes) {
114
+ return [
115
+ "Preview: public attributes",
116
+ "",
117
+ "Read-only USER.md tags:",
118
+ ...formatAttributeList(readOnlyTags),
119
+ "",
120
+ "Structured profile attributes to save:",
121
+ ...formatAttributeList(attributes),
122
+ ].join("\n");
123
+ }
124
+ function formatAttributeList(attributes) {
125
+ if (attributes.length === 0) {
126
+ return [" none"];
127
+ }
128
+ return attributes.map((attribute, index) => ` ${index + 1}. ${formatAttribute(attribute)}`);
129
+ }
130
+ function formatAttribute(attribute) {
131
+ if (attribute.kind === "tag") {
132
+ return `${attribute.label} (USER.md tag, read-only)`;
133
+ }
134
+ return `${attribute.key}: ${attribute.value}`;
135
+ }
136
+ function cancelledResult() {
137
+ return {
138
+ status: "cancelled",
139
+ message: CANCELLED_MESSAGE,
140
+ };
141
+ }
@@ -1,8 +1,27 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
3
  import { type SetupConfigWriter, type SetupPrompter } from "./setup-wizard.js";
4
+ export declare const LIBP2P_MESH_CLI_REGISTRATION: {
5
+ commands: string[];
6
+ descriptors: {
7
+ name: string;
8
+ description: string;
9
+ hasSubcommands: boolean;
10
+ }[];
11
+ };
12
+ export type ClosableSetupPrompter = SetupPrompter & {
13
+ close?: () => void;
14
+ };
4
15
  export type SetupCliDeps = {
5
16
  createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
6
17
  createWriter?: (api: OpenClawPluginApi) => SetupConfigWriter;
7
18
  };
8
19
  export declare function registerLibp2pMeshSetupCli(api: OpenClawPluginApi, deps?: SetupCliDeps): void;
20
+ export declare function registerLibp2pMeshSetupCommand(root: {
21
+ command(name: string): {
22
+ description(text: string): {
23
+ action(handler: () => Promise<void>): void;
24
+ };
25
+ };
26
+ }, api: OpenClawPluginApi, ctx: OpenClawPluginCliContext, deps?: SetupCliDeps): void;
27
+ export declare function createReadlinePrompter(): ClosableSetupPrompter;
@@ -5,38 +5,42 @@ const SETUP_CLI_AFTER_WRITE = {
5
5
  mode: "none",
6
6
  reason: "libp2p-mesh setup completed; restart manually to apply gateway changes.",
7
7
  };
8
+ export const LIBP2P_MESH_CLI_REGISTRATION = {
9
+ commands: ["libp2p-mesh"],
10
+ descriptors: [
11
+ {
12
+ name: "libp2p-mesh",
13
+ description: "Configure libp2p-mesh plugin.",
14
+ hasSubcommands: true,
15
+ },
16
+ ],
17
+ };
8
18
  export function registerLibp2pMeshSetupCli(api, deps = {}) {
9
19
  api.registerCli((ctx) => {
10
20
  const root = ctx.program
11
21
  .command("libp2p-mesh")
12
22
  .description("Configure libp2p-mesh plugin.");
13
- root
14
- .command("setup")
15
- .description("Run the libp2p-mesh setup wizard.")
16
- .action(async () => {
17
- const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter());
18
- const writer = deps.createWriter?.(api) ?? createOpenClawConfigWriter(api);
19
- try {
20
- const result = await runSetupWizard({
21
- currentConfig: ctx.config,
22
- prompter,
23
- writer,
24
- });
25
- prompter.print(result.message);
26
- }
27
- finally {
28
- prompter.close?.();
29
- }
30
- });
31
- }, {
32
- commands: ["libp2p-mesh"],
33
- descriptors: [
34
- {
35
- name: "libp2p-mesh",
36
- description: "Configure libp2p-mesh plugin.",
37
- hasSubcommands: true,
38
- },
39
- ],
23
+ registerLibp2pMeshSetupCommand(root, api, ctx, deps);
24
+ }, LIBP2P_MESH_CLI_REGISTRATION);
25
+ }
26
+ export function registerLibp2pMeshSetupCommand(root, api, ctx, deps = {}) {
27
+ root
28
+ .command("setup")
29
+ .description("Run the libp2p-mesh setup wizard.")
30
+ .action(async () => {
31
+ const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter());
32
+ const writer = deps.createWriter?.(api) ?? createOpenClawConfigWriter(api);
33
+ try {
34
+ const result = await runSetupWizard({
35
+ currentConfig: ctx.config,
36
+ prompter,
37
+ writer,
38
+ });
39
+ prompter.print(result.message);
40
+ }
41
+ finally {
42
+ prompter.close?.();
43
+ }
40
44
  });
41
45
  }
42
46
  function createOpenClawConfigWriter(api) {
@@ -57,7 +61,7 @@ function replaceConfig(draft, nextConfig) {
57
61
  }
58
62
  Object.assign(draft, structuredClone(nextConfig));
59
63
  }
60
- function createReadlinePrompter() {
64
+ export function createReadlinePrompter() {
61
65
  const readline = createInterface({ input, output });
62
66
  return {
63
67
  async confirm(message, defaultValue = false) {
@@ -38,6 +38,7 @@ export interface InstanceAnnouncePayload {
38
38
  instanceName?: string;
39
39
  multiaddrs: string[];
40
40
  pubkey?: string;
41
+ userPublicAttributes?: UserPublicAttribute[];
41
42
  announcedAt: number;
42
43
  }
43
44
  export interface UserMessagePayload {
@@ -72,12 +73,33 @@ export interface DeliveryAckPayload {
72
73
  deliveredAt: number;
73
74
  error?: string;
74
75
  }
76
+ export type UserPublicAttribute = {
77
+ kind: "tag";
78
+ value: string;
79
+ label: string;
80
+ source: "USER.md";
81
+ } | {
82
+ kind: "structured";
83
+ key: "group" | "project" | "role" | "skill" | "custom" | string;
84
+ value: string;
85
+ label: string;
86
+ source: "profile";
87
+ };
88
+ export type UserAttributeMatch = {
89
+ kind: "tag";
90
+ value: string;
91
+ } | {
92
+ kind: "structured";
93
+ key: string;
94
+ value: string;
95
+ };
75
96
  export interface InstancePeerRecord {
76
97
  instanceId: string;
77
98
  peerId: string;
78
99
  instanceName?: string;
79
100
  multiaddrs: string[];
80
101
  pubkey?: string;
102
+ userPublicAttributes?: UserPublicAttribute[];
81
103
  lastSeenAt: number;
82
104
  lastAnnouncedAt: number;
83
105
  source: "announce";
@@ -97,6 +119,26 @@ export interface InstancePeerStore {
97
119
  peerIdSharedBy: string[];
98
120
  }>;
99
121
  }
122
+ export type UserAttributeMessageTarget = {
123
+ instanceId: string;
124
+ instanceName?: string;
125
+ peerId: string;
126
+ matchedAttribute: UserPublicAttribute;
127
+ };
128
+ export type UserAttributeMessageDeliveryResult = UserAttributeMessageTarget & {
129
+ sent: boolean;
130
+ delivered: boolean;
131
+ error?: string;
132
+ };
133
+ export type UserAttributeMessageResult = {
134
+ matched: number;
135
+ sent: number;
136
+ delivered: number;
137
+ failed: number;
138
+ targets?: UserAttributeMessageTarget[];
139
+ results?: UserAttributeMessageDeliveryResult[];
140
+ error?: string;
141
+ };
100
142
  export interface InboundDeliveryRequest {
101
143
  channel: string;
102
144
  target: string;
@@ -119,6 +161,24 @@ export interface InboundDeliveryResult {
119
161
  export interface InboundDeliveryAdapter {
120
162
  deliver(request: InboundDeliveryRequest): Promise<InboundDeliveryResult>;
121
163
  }
164
+ export type InstanceRouterOptions = {
165
+ mesh: MeshNetwork;
166
+ store: InstancePeerStore;
167
+ delivery: InboundDeliveryAdapter;
168
+ config?: MeshConfig;
169
+ logger?: {
170
+ info?: (message: string) => void;
171
+ debug?: (message: string) => void;
172
+ warn?: (message: string) => void;
173
+ error?: (message: string) => void;
174
+ };
175
+ userAttributeSource?: {
176
+ loadTags(): Promise<UserPublicAttribute[]>;
177
+ };
178
+ userProfileStore?: {
179
+ listAttributes(): Promise<UserPublicAttribute[]>;
180
+ };
181
+ };
122
182
  export interface InstanceRouter {
123
183
  start(): Promise<void>;
124
184
  stop(): Promise<void>;
@@ -137,6 +197,9 @@ export interface InstanceRouter {
137
197
  deliveryResults?: DeliveryTargetResult[];
138
198
  error?: string;
139
199
  }>;
200
+ sendUserAttributeMessage(match: UserAttributeMatch, message: string, options?: {
201
+ dryRun?: boolean;
202
+ }): Promise<UserAttributeMessageResult>;
140
203
  }
141
204
  export interface MeshConfig {
142
205
  listenAddrs?: string[];
@@ -0,0 +1,6 @@
1
+ import type { UserAttributeMatch, UserPublicAttribute } from "./types.js";
2
+ export declare function normalizeAttributeValue(value: string): string;
3
+ export declare function normalizeAttributeKey(key: string): string;
4
+ export declare function normalizeUserPublicAttribute(value: unknown): UserPublicAttribute | undefined;
5
+ export declare function mergeUserPublicAttributes(userMdTags: UserPublicAttribute[], profileAttributes: UserPublicAttribute[]): UserPublicAttribute[];
6
+ export declare function matchesUserAttribute(attribute: UserPublicAttribute, match: UserAttributeMatch): boolean;