@indexnetwork/protocol 14.3.2-rc.477.1 → 16.1.1-rc.478.1

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/IMPLEMENTATION.md +50 -52
  3. package/STABILITY.md +3 -3
  4. package/dist/chat/chat.agent.js +2 -2
  5. package/dist/chat/chat.graph.d.ts +41 -1
  6. package/dist/chat/chat.graph.js +108 -127
  7. package/dist/discovery/hyde.frame.d.ts +4 -4
  8. package/dist/discovery/hyde.graph.d.ts +71 -12
  9. package/dist/discovery/hyde.graph.js +337 -335
  10. package/dist/discovery/lens.inferrer.d.ts +6 -6
  11. package/dist/enrichment/enrichment.graph.d.ts +127 -9
  12. package/dist/enrichment/enrichment.graph.js +633 -655
  13. package/dist/enrichment/enrichment.state.d.ts +1 -1
  14. package/dist/enrichment/enrichment.tools.context-read.d.ts +10 -0
  15. package/dist/enrichment/enrichment.tools.context-read.js +353 -0
  16. package/dist/enrichment/enrichment.tools.context-write.d.ts +9 -0
  17. package/dist/enrichment/enrichment.tools.context-write.js +417 -0
  18. package/dist/enrichment/enrichment.tools.d.ts +11 -1
  19. package/dist/enrichment/enrichment.tools.helpers.d.ts +139 -0
  20. package/dist/enrichment/enrichment.tools.helpers.js +234 -0
  21. package/dist/enrichment/enrichment.tools.js +16 -975
  22. package/dist/index.d.ts +27 -48
  23. package/dist/index.js +17 -27
  24. package/dist/intents/application/intent.graph.d.ts +71 -67
  25. package/dist/intents/application/intent.graph.execute.d.ts +59 -0
  26. package/dist/intents/application/intent.graph.execute.js +301 -0
  27. package/dist/intents/application/intent.graph.infer.d.ts +44 -0
  28. package/dist/intents/application/intent.graph.infer.js +97 -0
  29. package/dist/intents/application/intent.graph.js +96 -888
  30. package/dist/intents/application/intent.graph.reconcile.d.ts +71 -0
  31. package/dist/intents/application/intent.graph.reconcile.js +274 -0
  32. package/dist/intents/application/intent.graph.shared.d.ts +70 -0
  33. package/dist/intents/application/intent.graph.shared.js +153 -0
  34. package/dist/intents/domain/intent.state.d.ts +1 -1
  35. package/dist/maintenance/maintenance.graph.d.ts +66 -3
  36. package/dist/maintenance/maintenance.graph.js +155 -156
  37. package/dist/mcp/mcp.authorization-policy.d.ts +24 -28
  38. package/dist/mcp/mcp.authorization-policy.js +14 -34
  39. package/dist/mcp/mcp.server.d.ts +3 -3
  40. package/dist/mcp/mcp.server.js +12 -16
  41. package/dist/negotiations/application/negotiation.candidates.d.ts +83 -0
  42. package/dist/negotiations/application/negotiation.candidates.js +162 -0
  43. package/dist/negotiations/application/negotiation.graph.d.ts +91 -157
  44. package/dist/negotiations/application/negotiation.graph.finalize.d.ts +5 -0
  45. package/dist/negotiations/application/negotiation.graph.finalize.js +280 -0
  46. package/dist/negotiations/application/negotiation.graph.init.d.ts +54 -0
  47. package/dist/negotiations/application/negotiation.graph.init.js +227 -0
  48. package/dist/negotiations/application/negotiation.graph.js +69 -1388
  49. package/dist/negotiations/application/negotiation.graph.screen.d.ts +23 -0
  50. package/dist/negotiations/application/negotiation.graph.screen.js +108 -0
  51. package/dist/negotiations/application/negotiation.graph.shared.d.ts +75 -0
  52. package/dist/negotiations/application/negotiation.graph.shared.js +125 -0
  53. package/dist/negotiations/application/negotiation.graph.turn.d.ts +105 -0
  54. package/dist/negotiations/application/negotiation.graph.turn.js +484 -0
  55. package/dist/negotiations/domain/negotiation.state.d.ts +1 -1
  56. package/dist/negotiations/domain/negotiation.state.js +0 -1
  57. package/dist/networks/application/indexer.graph.d.ts +165 -7
  58. package/dist/networks/application/indexer.graph.js +339 -388
  59. package/dist/networks/application/indexer.state.d.ts +1 -1
  60. package/dist/networks/application/membership.graph.d.ts +83 -5
  61. package/dist/networks/application/membership.graph.js +177 -207
  62. package/dist/networks/application/network.graph.d.ts +148 -5
  63. package/dist/networks/application/network.graph.js +249 -278
  64. package/dist/networks/domain/membership.state.d.ts +1 -1
  65. package/dist/networks/domain/network.state.d.ts +1 -1
  66. package/dist/opportunities/application/delivery-card.cache.d.ts +1 -1
  67. package/dist/opportunities/application/delivery-card.cache.js +2 -2
  68. package/dist/opportunities/application/index.d.ts +2 -2
  69. package/dist/opportunities/application/index.js +2 -2
  70. package/dist/opportunities/application/opportunity.evaluator.js +1 -1
  71. package/dist/opportunities/application/opportunity.graph.d.ts +686 -473
  72. package/dist/opportunities/application/opportunity.graph.discovery-strategies.d.ts +109 -0
  73. package/dist/opportunities/application/opportunity.graph.discovery-strategies.js +451 -0
  74. package/dist/opportunities/application/opportunity.graph.discovery.d.ts +69 -0
  75. package/dist/opportunities/application/opportunity.graph.discovery.js +397 -0
  76. package/dist/opportunities/application/opportunity.graph.evaluation.d.ts +82 -0
  77. package/dist/opportunities/application/opportunity.graph.evaluation.js +608 -0
  78. package/dist/opportunities/application/opportunity.graph.js +120 -3690
  79. package/dist/opportunities/application/opportunity.graph.modes.d.ts +538 -0
  80. package/dist/opportunities/application/opportunity.graph.modes.js +538 -0
  81. package/dist/opportunities/application/opportunity.graph.negotiate.d.ts +109 -0
  82. package/dist/opportunities/application/opportunity.graph.negotiate.js +392 -0
  83. package/dist/opportunities/application/opportunity.graph.persist-node.d.ts +93 -0
  84. package/dist/opportunities/application/opportunity.graph.persist-node.js +765 -0
  85. package/dist/opportunities/application/opportunity.graph.prep.d.ts +150 -0
  86. package/dist/opportunities/application/opportunity.graph.prep.js +381 -0
  87. package/dist/opportunities/application/opportunity.graph.shared.d.ts +173 -0
  88. package/dist/opportunities/application/opportunity.graph.shared.js +199 -0
  89. package/dist/opportunities/application/opportunity.presentation.d.ts +399 -0
  90. package/dist/opportunities/application/{opportunity.presenter.js → opportunity.presentation.js} +735 -9
  91. package/dist/opportunities/application/opportunity.tools.cards.d.ts +152 -0
  92. package/dist/opportunities/application/opportunity.tools.cards.js +235 -0
  93. package/dist/opportunities/application/opportunity.tools.d.ts +8 -114
  94. package/dist/opportunities/application/opportunity.tools.js +28 -711
  95. package/dist/opportunities/application/opportunity.tools.list.d.ts +10 -0
  96. package/dist/opportunities/application/opportunity.tools.list.js +493 -0
  97. package/dist/opportunities/domain/index.d.ts +3 -3
  98. package/dist/opportunities/domain/index.js +3 -3
  99. package/dist/opportunities/domain/opportunity.state.d.ts +19 -19
  100. package/dist/opportunities/index.d.ts +6 -6
  101. package/dist/opportunities/index.js +4 -4
  102. package/dist/opportunities/ports/opportunity.tools.port.d.ts +1 -1
  103. package/dist/opportunities/radar/radar.graph.d.ts +57 -13
  104. package/dist/opportunities/radar/radar.graph.js +470 -471
  105. package/dist/premises/premise.graph.d.ts +88 -20
  106. package/dist/premises/premise.graph.js +207 -218
  107. package/dist/questions/domain/question.schema.d.ts +40 -40
  108. package/dist/shared/agent/tool.helpers.d.ts +21 -0
  109. package/dist/shared/agent/tool.registry.d.ts +4 -3
  110. package/dist/shared/agent/tool.registry.js +1 -1
  111. package/dist/shared/interfaces/database.capabilities.d.ts +100 -0
  112. package/dist/shared/interfaces/database.capabilities.js +7 -0
  113. package/dist/shared/interfaces/database.entities.d.ts +533 -0
  114. package/dist/shared/interfaces/database.entities.js +10 -0
  115. package/dist/shared/interfaces/database.identity-queries.d.ts +294 -0
  116. package/dist/shared/interfaces/database.identity-queries.js +4 -0
  117. package/dist/shared/interfaces/database.interface.d.ts +15 -2277
  118. package/dist/shared/interfaces/database.interface.js +8 -0
  119. package/dist/shared/interfaces/database.member-queries.d.ts +228 -0
  120. package/dist/shared/interfaces/database.member-queries.js +4 -0
  121. package/dist/shared/interfaces/database.negotiation.d.ts +305 -0
  122. package/dist/shared/interfaces/database.negotiation.js +26 -0
  123. package/dist/shared/interfaces/database.network-queries.d.ts +277 -0
  124. package/dist/shared/interfaces/database.network-queries.js +4 -0
  125. package/dist/shared/interfaces/database.opportunity-queries.d.ts +285 -0
  126. package/dist/shared/interfaces/database.opportunity-queries.js +4 -0
  127. package/dist/shared/interfaces/database.port.d.ts +322 -0
  128. package/dist/shared/interfaces/database.port.js +29 -0
  129. package/dist/shared/schemas/discovery-question.schema.d.ts +14 -14
  130. package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
  131. package/package.json +2 -15
  132. package/dist/opportunities/application/opportunity.card-presentation.d.ts +0 -44
  133. package/dist/opportunities/application/opportunity.card-presentation.js +0 -93
  134. package/dist/opportunities/application/opportunity.presenter.d.ts +0 -156
  135. package/dist/opportunities/domain/opportunity.presentation-cache.d.ts +0 -5
  136. package/dist/opportunities/domain/opportunity.presentation-cache.js +0 -11
  137. package/dist/opportunities/domain/opportunity.presentation.d.ts +0 -76
  138. package/dist/opportunities/domain/opportunity.presentation.js +0 -516
  139. package/dist/opportunities/domain/opportunity.safe-presentation.d.ts +0 -104
  140. package/dist/opportunities/domain/opportunity.safe-presentation.js +0 -102
@@ -75,7 +75,7 @@ export declare const IntentNetworkGraphState: import("@langchain/langgraph").Ann
75
75
  /** Target intent for assign/read-by-intent. From tool arg. */
76
76
  intentId: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
77
77
  /** Operation mode. */
78
- operationMode: import("@langchain/langgraph").BaseChannel<"delete" | "create" | "read", "delete" | "create" | "read" | import("@langchain/langgraph").OverwriteValue<"delete" | "create" | "read">, unknown>;
78
+ operationMode: import("@langchain/langgraph").BaseChannel<"create" | "delete" | "read", "create" | "delete" | "read" | import("@langchain/langgraph").OverwriteValue<"create" | "delete" | "read">, unknown>;
79
79
  /**
80
80
  * When true, skip LLM evaluation and assign directly (manual_override).
81
81
  * When false, run IntentIndexer evaluation (automatic mode).
@@ -1,4 +1,5 @@
1
1
  import type { NetworkMembershipGraphDatabase } from "../ports/index.js";
2
+ import { NetworkMembershipGraphState } from "../domain/index.js";
2
3
  /**
3
4
  * Factory class to build and compile the Network Membership Graph.
4
5
  *
@@ -28,13 +29,20 @@ import type { NetworkMembershipGraphDatabase } from "../ports/index.js";
28
29
  *
29
30
  * IND-546: canonical home — previously network/membership/membership.graph.ts.
30
31
  */
32
+ /** The graph's channel state, as every node sees it. */
33
+ export type NetworkMembershipState = typeof NetworkMembershipGraphState.State;
34
+ /** Everything the membership nodes reach for. */
35
+ export interface NetworkMembershipGraphDeps {
36
+ database: NetworkMembershipGraphDatabase;
37
+ }
31
38
  export declare class NetworkMembershipGraphFactory {
32
- private database;
39
+ /** Resolved dependency bag shared by every node. */
40
+ readonly deps: NetworkMembershipGraphDeps;
33
41
  constructor(database: NetworkMembershipGraphDatabase);
34
42
  createGraph(): import("@langchain/langgraph").CompiledStateGraph<{
35
43
  userId: string;
36
44
  networkId: string;
37
- operationMode: "delete" | "create" | "read";
45
+ operationMode: "create" | "delete" | "read";
38
46
  targetUserId: string | undefined;
39
47
  readResult: {
40
48
  networkId: string;
@@ -57,7 +65,7 @@ export declare class NetworkMembershipGraphFactory {
57
65
  }, {
58
66
  userId?: string | undefined;
59
67
  networkId?: string | undefined;
60
- operationMode?: "delete" | "create" | "read" | import("@langchain/langgraph").OverwriteValue<"delete" | "create" | "read"> | undefined;
68
+ operationMode?: "create" | "delete" | "read" | import("@langchain/langgraph").OverwriteValue<"create" | "delete" | "read"> | undefined;
61
69
  targetUserId?: string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined;
62
70
  readResult?: {
63
71
  networkId: string;
@@ -103,7 +111,7 @@ export declare class NetworkMembershipGraphFactory {
103
111
  (): import("@langchain/langgraph").LastValue<string>;
104
112
  Root: <S extends import("@langchain/langgraph").StateDefinition>(sd: S) => import("@langchain/langgraph").AnnotationRoot<S>;
105
113
  };
106
- operationMode: import("@langchain/langgraph").BaseChannel<"delete" | "create" | "read", "delete" | "create" | "read" | import("@langchain/langgraph").OverwriteValue<"delete" | "create" | "read">, unknown>;
114
+ operationMode: import("@langchain/langgraph").BaseChannel<"create" | "delete" | "read", "create" | "delete" | "read" | import("@langchain/langgraph").OverwriteValue<"create" | "delete" | "read">, unknown>;
107
115
  targetUserId: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
108
116
  readResult: import("@langchain/langgraph").BaseChannel<{
109
117
  networkId: string;
@@ -164,7 +172,7 @@ export declare class NetworkMembershipGraphFactory {
164
172
  (): import("@langchain/langgraph").LastValue<string>;
165
173
  Root: <S extends import("@langchain/langgraph").StateDefinition>(sd: S) => import("@langchain/langgraph").AnnotationRoot<S>;
166
174
  };
167
- operationMode: import("@langchain/langgraph").BaseChannel<"delete" | "create" | "read", "delete" | "create" | "read" | import("@langchain/langgraph").OverwriteValue<"delete" | "create" | "read">, unknown>;
175
+ operationMode: import("@langchain/langgraph").BaseChannel<"create" | "delete" | "read", "create" | "delete" | "read" | import("@langchain/langgraph").OverwriteValue<"create" | "delete" | "read">, unknown>;
168
176
  targetUserId: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
169
177
  readResult: import("@langchain/langgraph").BaseChannel<{
170
178
  networkId: string;
@@ -268,3 +276,73 @@ export declare class NetworkMembershipGraphFactory {
268
276
  };
269
277
  }, unknown, unknown, []>;
270
278
  }
279
+ /**
280
+ * Add Member Node: Add a user as a member of a network.
281
+ *
282
+ * Two sub-paths:
283
+ * 1. Self-join (targetUserId === userId): only allowed for 'anyone' join policy.
284
+ * 2. Invite others (targetUserId !== userId): caller must be a member; owner-only
285
+ * for invite_only networks.
286
+ */
287
+ export declare function addMemberNode(state: NetworkMembershipState, deps: NetworkMembershipGraphDeps): Promise<{
288
+ mutationResult: {
289
+ success: boolean;
290
+ error: string;
291
+ message?: undefined;
292
+ };
293
+ } | {
294
+ mutationResult: {
295
+ success: boolean;
296
+ message: string;
297
+ error?: undefined;
298
+ };
299
+ }>;
300
+ /**
301
+ * List Members Node: List all members of a network.
302
+ * Validates that the caller is a member before returning results.
303
+ */
304
+ export declare function listMembersNode(state: NetworkMembershipState, deps: NetworkMembershipGraphDeps): Promise<{
305
+ readResult: {
306
+ networkId: string;
307
+ count: number;
308
+ members: never[];
309
+ };
310
+ error: string;
311
+ } | {
312
+ readResult: {
313
+ networkId: string;
314
+ count: number;
315
+ members: {
316
+ userId: string;
317
+ name: string;
318
+ avatar: string | null;
319
+ permissions: string[];
320
+ intentCount: number;
321
+ joinedAt: Date;
322
+ }[];
323
+ };
324
+ error?: undefined;
325
+ } | {
326
+ error: string;
327
+ readResult?: undefined;
328
+ }>;
329
+ /**
330
+ * Remove Member Node: Remove a member from a network (owner only).
331
+ *
332
+ * The owner themselves cannot be removed via this path. To transfer or
333
+ * remove the owner, delete the network or promote another member first.
334
+ */
335
+ export declare function removeMemberNode(state: NetworkMembershipState, deps: NetworkMembershipGraphDeps): Promise<{
336
+ mutationResult: {
337
+ success: boolean;
338
+ error: string;
339
+ message?: undefined;
340
+ };
341
+ } | {
342
+ mutationResult: {
343
+ success: boolean;
344
+ message: string;
345
+ error?: undefined;
346
+ };
347
+ }>;
348
+ export declare function routeByMode(state: NetworkMembershipState): string;
@@ -3,219 +3,17 @@ import { protocolLogger } from "../../shared/observability/protocol.logger.js";
3
3
  import { timed } from "../../shared/observability/performance.js";
4
4
  import { NetworkMembershipGraphState } from "../domain/index.js";
5
5
  const logger = protocolLogger("NetworkMembershipGraphFactory");
6
- /**
7
- * Factory class to build and compile the Network Membership Graph.
8
- *
9
- * Handles CRUD operations for the index_members table:
10
- * - create: Add a member to a network (validates join policy and ownership)
11
- * - read: List members of a network (validates caller is member)
12
- * - delete: Remove a member from a network (owner-only)
13
- *
14
- * ## Membership authority policy
15
- *
16
- * Self-join (`targetUserId === userId`):
17
- * - Allowed only when `joinPolicy: 'anyone'`.
18
- * - Idempotent: already-a-member returns success.
19
- *
20
- * Invite others (`targetUserId !== userId`):
21
- * - Caller must be a member of the network.
22
- * - For `invite_only` networks, caller must also be the owner.
23
- * - Idempotent: already-a-member returns success.
24
- *
25
- * Remove member:
26
- * - Caller must be the network owner.
27
- * - Cannot remove the owner (delete the network instead).
28
- * - Cannot remove yourself via this path (use leave-network flow).
29
- *
30
- * Flow:
31
- * START → routerNode → {addMemberNode | listMembersNode | removeMemberNode} → END
32
- *
33
- * IND-546: canonical home — previously network/membership/membership.graph.ts.
34
- */
35
6
  export class NetworkMembershipGraphFactory {
36
7
  constructor(database) {
37
- this.database = database;
8
+ this.deps = { database };
38
9
  }
39
10
  createGraph() {
40
- // --- NODE DEFINITIONS ---
41
- /**
42
- * Add Member Node: Add a user as a member of a network.
43
- *
44
- * Two sub-paths:
45
- * 1. Self-join (targetUserId === userId): only allowed for 'anyone' join policy.
46
- * 2. Invite others (targetUserId !== userId): caller must be a member; owner-only
47
- * for invite_only networks.
48
- */
49
- const addMemberNode = async (state) => {
50
- return timed("NetworkMembershipGraph.addMember", async () => {
51
- logger.verbose("Add member to network", {
52
- userId: state.userId,
53
- networkId: state.networkId,
54
- targetUserId: state.targetUserId,
55
- });
56
- if (!state.targetUserId) {
57
- return { mutationResult: { success: false, error: "targetUserId is required." } };
58
- }
59
- try {
60
- const networkRecord = await this.database.getNetworkWithPermissions(state.networkId);
61
- if (!networkRecord) {
62
- return { mutationResult: { success: false, error: "Network not found." } };
63
- }
64
- const joinPolicy = networkRecord.permissions.joinPolicy;
65
- const isSelfJoin = state.targetUserId === state.userId;
66
- if (isSelfJoin) {
67
- // Self-join: only allowed for open networks
68
- if (joinPolicy !== 'anyone') {
69
- return {
70
- mutationResult: {
71
- success: false,
72
- error: "This network is invite-only. You cannot join without an invitation from an existing member.",
73
- },
74
- };
75
- }
76
- const result = await this.database.addMemberToNetwork(state.networkId, state.targetUserId, 'member');
77
- if (result.alreadyMember) {
78
- return { mutationResult: { success: true, message: "You are already a member of this network." } };
79
- }
80
- return { mutationResult: { success: true, message: `You have joined "${networkRecord.title}".` } };
81
- }
82
- // Inviting others: caller must be a member first
83
- const isMember = await this.database.isNetworkMember(state.networkId, state.userId);
84
- if (!isMember) {
85
- return { mutationResult: { success: false, error: "You must be a member of that network to add others." } };
86
- }
87
- if (joinPolicy === 'invite_only') {
88
- const isOwner = await this.database.isIndexOwner(state.networkId, state.userId);
89
- if (!isOwner) {
90
- return { mutationResult: { success: false, error: "Only the network owner can add members when the network is invite-only." } };
91
- }
92
- }
93
- const result = await this.database.addMemberToNetwork(state.networkId, state.targetUserId, 'member');
94
- if (result.alreadyMember) {
95
- return { mutationResult: { success: true, message: "That user is already a member of this network." } };
96
- }
97
- return { mutationResult: { success: true, message: "Member added to the network." } };
98
- }
99
- catch (err) {
100
- logger.error("Add member failed", { error: err });
101
- return {
102
- mutationResult: {
103
- success: false,
104
- error: err instanceof Error ? err.message : "Failed to add member.",
105
- },
106
- };
107
- }
108
- });
109
- };
110
- /**
111
- * List Members Node: List all members of a network.
112
- * Validates that the caller is a member before returning results.
113
- */
114
- const listMembersNode = async (state) => {
115
- return timed("NetworkMembershipGraph.listMembers", async () => {
116
- logger.verbose("List network members", {
117
- userId: state.userId,
118
- networkId: state.networkId,
119
- });
120
- try {
121
- const isMember = await this.database.isNetworkMember(state.networkId, state.userId);
122
- if (!isMember) {
123
- return {
124
- readResult: {
125
- networkId: state.networkId,
126
- count: 0,
127
- members: [],
128
- },
129
- error: "Network not found or you are not a member.",
130
- };
131
- }
132
- const members = await this.database.getNetworkMembersForMember(state.networkId, state.userId);
133
- return {
134
- readResult: {
135
- networkId: state.networkId,
136
- count: members.length,
137
- members: members.map((m) => ({
138
- userId: m.userId,
139
- name: m.name,
140
- avatar: m.avatar,
141
- permissions: m.permissions,
142
- intentCount: m.intentCount,
143
- joinedAt: m.joinedAt,
144
- })),
145
- },
146
- };
147
- }
148
- catch (err) {
149
- logger.error("List members failed", { error: err });
150
- if (err instanceof Error && err.message === "Access denied: Not a member of this network") {
151
- return { error: "You must be a member of that network." };
152
- }
153
- return { error: "Failed to fetch network members." };
154
- }
155
- });
156
- };
157
- /**
158
- * Remove Member Node: Remove a member from a network (owner only).
159
- *
160
- * The owner themselves cannot be removed via this path. To transfer or
161
- * remove the owner, delete the network or promote another member first.
162
- */
163
- const removeMemberNode = async (state) => {
164
- return timed("NetworkMembershipGraph.removeMember", async () => {
165
- logger.verbose("Remove member from network", {
166
- userId: state.userId,
167
- networkId: state.networkId,
168
- targetUserId: state.targetUserId,
169
- });
170
- if (!state.targetUserId) {
171
- return { mutationResult: { success: false, error: "targetUserId is required." } };
172
- }
173
- // Cannot remove yourself via this flow
174
- if (state.targetUserId === state.userId) {
175
- return { mutationResult: { success: false, error: "You cannot remove yourself. Use 'leave network' instead." } };
176
- }
177
- try {
178
- const isOwner = await this.database.isIndexOwner(state.networkId, state.userId);
179
- if (!isOwner) {
180
- return { mutationResult: { success: false, error: "Only the network owner can remove members." } };
181
- }
182
- const result = await this.database.removeMemberFromIndex(state.networkId, state.targetUserId);
183
- if (result.wasOwner) {
184
- return { mutationResult: { success: false, error: "Cannot remove the network owner." } };
185
- }
186
- if (result.notMember) {
187
- return { mutationResult: { success: false, error: "User is not a member of this network." } };
188
- }
189
- if (!result.success) {
190
- return { mutationResult: { success: false, error: "Failed to remove member." } };
191
- }
192
- return {
193
- mutationResult: {
194
- success: true,
195
- message: "Member removed from the network.",
196
- },
197
- };
198
- }
199
- catch (err) {
200
- logger.error("Remove member failed", { error: err });
201
- return { mutationResult: { success: false, error: "Failed to remove member." } };
202
- }
203
- });
204
- };
205
- // --- CONDITIONAL ROUTING ---
206
- const routeByMode = (state) => {
207
- switch (state.operationMode) {
208
- case 'create': return 'add_member';
209
- case 'read': return 'list_members';
210
- case 'delete': return 'remove_member';
211
- default: return 'list_members';
212
- }
213
- };
11
+ const deps = this.deps;
214
12
  // --- GRAPH ASSEMBLY ---
215
13
  const workflow = new StateGraph(NetworkMembershipGraphState)
216
- .addNode("add_member", addMemberNode)
217
- .addNode("list_members", listMembersNode)
218
- .addNode("remove_member", removeMemberNode)
14
+ .addNode("add_member", (state) => addMemberNode(state, deps))
15
+ .addNode("list_members", (state) => listMembersNode(state, deps))
16
+ .addNode("remove_member", (state) => removeMemberNode(state, deps))
219
17
  .addConditionalEdges(START, routeByMode, {
220
18
  add_member: "add_member",
221
19
  list_members: "list_members",
@@ -227,3 +25,175 @@ export class NetworkMembershipGraphFactory {
227
25
  return workflow.compile();
228
26
  }
229
27
  }
28
+ /**
29
+ * Add Member Node: Add a user as a member of a network.
30
+ *
31
+ * Two sub-paths:
32
+ * 1. Self-join (targetUserId === userId): only allowed for 'anyone' join policy.
33
+ * 2. Invite others (targetUserId !== userId): caller must be a member; owner-only
34
+ * for invite_only networks.
35
+ */
36
+ export async function addMemberNode(state, deps) {
37
+ return timed("NetworkMembershipGraph.addMember", async () => {
38
+ logger.verbose("Add member to network", {
39
+ userId: state.userId,
40
+ networkId: state.networkId,
41
+ targetUserId: state.targetUserId,
42
+ });
43
+ if (!state.targetUserId) {
44
+ return { mutationResult: { success: false, error: "targetUserId is required." } };
45
+ }
46
+ try {
47
+ const networkRecord = await deps.database.getNetworkWithPermissions(state.networkId);
48
+ if (!networkRecord) {
49
+ return { mutationResult: { success: false, error: "Network not found." } };
50
+ }
51
+ const joinPolicy = networkRecord.permissions.joinPolicy;
52
+ const isSelfJoin = state.targetUserId === state.userId;
53
+ if (isSelfJoin) {
54
+ // Self-join: only allowed for open networks
55
+ if (joinPolicy !== 'anyone') {
56
+ return {
57
+ mutationResult: {
58
+ success: false,
59
+ error: "This network is invite-only. You cannot join without an invitation from an existing member.",
60
+ },
61
+ };
62
+ }
63
+ const result = await deps.database.addMemberToNetwork(state.networkId, state.targetUserId, 'member');
64
+ if (result.alreadyMember) {
65
+ return { mutationResult: { success: true, message: "You are already a member of this network." } };
66
+ }
67
+ return { mutationResult: { success: true, message: `You have joined "${networkRecord.title}".` } };
68
+ }
69
+ // Inviting others: caller must be a member first
70
+ const isMember = await deps.database.isNetworkMember(state.networkId, state.userId);
71
+ if (!isMember) {
72
+ return { mutationResult: { success: false, error: "You must be a member of that network to add others." } };
73
+ }
74
+ if (joinPolicy === 'invite_only') {
75
+ const isOwner = await deps.database.isIndexOwner(state.networkId, state.userId);
76
+ if (!isOwner) {
77
+ return { mutationResult: { success: false, error: "Only the network owner can add members when the network is invite-only." } };
78
+ }
79
+ }
80
+ const result = await deps.database.addMemberToNetwork(state.networkId, state.targetUserId, 'member');
81
+ if (result.alreadyMember) {
82
+ return { mutationResult: { success: true, message: "That user is already a member of this network." } };
83
+ }
84
+ return { mutationResult: { success: true, message: "Member added to the network." } };
85
+ }
86
+ catch (err) {
87
+ logger.error("Add member failed", { error: err });
88
+ return {
89
+ mutationResult: {
90
+ success: false,
91
+ error: err instanceof Error ? err.message : "Failed to add member.",
92
+ },
93
+ };
94
+ }
95
+ });
96
+ }
97
+ /**
98
+ * List Members Node: List all members of a network.
99
+ * Validates that the caller is a member before returning results.
100
+ */
101
+ export async function listMembersNode(state, deps) {
102
+ return timed("NetworkMembershipGraph.listMembers", async () => {
103
+ logger.verbose("List network members", {
104
+ userId: state.userId,
105
+ networkId: state.networkId,
106
+ });
107
+ try {
108
+ const isMember = await deps.database.isNetworkMember(state.networkId, state.userId);
109
+ if (!isMember) {
110
+ return {
111
+ readResult: {
112
+ networkId: state.networkId,
113
+ count: 0,
114
+ members: [],
115
+ },
116
+ error: "Network not found or you are not a member.",
117
+ };
118
+ }
119
+ const members = await deps.database.getNetworkMembersForMember(state.networkId, state.userId);
120
+ return {
121
+ readResult: {
122
+ networkId: state.networkId,
123
+ count: members.length,
124
+ members: members.map((m) => ({
125
+ userId: m.userId,
126
+ name: m.name,
127
+ avatar: m.avatar,
128
+ permissions: m.permissions,
129
+ intentCount: m.intentCount,
130
+ joinedAt: m.joinedAt,
131
+ })),
132
+ },
133
+ };
134
+ }
135
+ catch (err) {
136
+ logger.error("List members failed", { error: err });
137
+ if (err instanceof Error && err.message === "Access denied: Not a member of this network") {
138
+ return { error: "You must be a member of that network." };
139
+ }
140
+ return { error: "Failed to fetch network members." };
141
+ }
142
+ });
143
+ }
144
+ /**
145
+ * Remove Member Node: Remove a member from a network (owner only).
146
+ *
147
+ * The owner themselves cannot be removed via this path. To transfer or
148
+ * remove the owner, delete the network or promote another member first.
149
+ */
150
+ export async function removeMemberNode(state, deps) {
151
+ return timed("NetworkMembershipGraph.removeMember", async () => {
152
+ logger.verbose("Remove member from network", {
153
+ userId: state.userId,
154
+ networkId: state.networkId,
155
+ targetUserId: state.targetUserId,
156
+ });
157
+ if (!state.targetUserId) {
158
+ return { mutationResult: { success: false, error: "targetUserId is required." } };
159
+ }
160
+ // Cannot remove yourself via this flow
161
+ if (state.targetUserId === state.userId) {
162
+ return { mutationResult: { success: false, error: "You cannot remove yourself. Use 'leave network' instead." } };
163
+ }
164
+ try {
165
+ const isOwner = await deps.database.isIndexOwner(state.networkId, state.userId);
166
+ if (!isOwner) {
167
+ return { mutationResult: { success: false, error: "Only the network owner can remove members." } };
168
+ }
169
+ const result = await deps.database.removeMemberFromIndex(state.networkId, state.targetUserId);
170
+ if (result.wasOwner) {
171
+ return { mutationResult: { success: false, error: "Cannot remove the network owner." } };
172
+ }
173
+ if (result.notMember) {
174
+ return { mutationResult: { success: false, error: "User is not a member of this network." } };
175
+ }
176
+ if (!result.success) {
177
+ return { mutationResult: { success: false, error: "Failed to remove member." } };
178
+ }
179
+ return {
180
+ mutationResult: {
181
+ success: true,
182
+ message: "Member removed from the network.",
183
+ },
184
+ };
185
+ }
186
+ catch (err) {
187
+ logger.error("Remove member failed", { error: err });
188
+ return { mutationResult: { success: false, error: "Failed to remove member." } };
189
+ }
190
+ });
191
+ }
192
+ export function routeByMode(state) {
193
+ switch (state.operationMode) {
194
+ case 'create': return 'add_member';
195
+ case 'read': return 'list_members';
196
+ case 'delete': return 'remove_member';
197
+ default: return 'list_members';
198
+ }
199
+ }