@tjamescouch/agentchat 0.10.0 → 0.12.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/bin/agentchat.js +192 -4
- package/lib/client.js +137 -4
- package/lib/elo_swarm.py +569 -0
- package/lib/escrow-hooks.js +237 -0
- package/lib/identity.js +134 -2
- package/lib/proposals.js +33 -8
- package/lib/protocol.js +115 -6
- package/lib/reputation.js +214 -14
- package/lib/server-directory.js +181 -0
- package/lib/server.js +368 -19
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EscrowHooks - Event system for external escrow integration
|
|
3
|
+
*
|
|
4
|
+
* Allows external systems (blockchain, multi-sig, compliance) to hook into
|
|
5
|
+
* escrow lifecycle events without modifying core AgentChat code.
|
|
6
|
+
*
|
|
7
|
+
* Events:
|
|
8
|
+
* escrow:created - Escrow created when proposal accepted with stakes
|
|
9
|
+
* escrow:released - Escrow released (expired, cancelled)
|
|
10
|
+
* settlement:completion - Proposal completed, stakes returned
|
|
11
|
+
* settlement:dispute - Proposal disputed, stakes transferred/burned
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const EscrowEvent = {
|
|
15
|
+
CREATED: 'escrow:created',
|
|
16
|
+
RELEASED: 'escrow:released',
|
|
17
|
+
COMPLETION_SETTLED: 'settlement:completion',
|
|
18
|
+
DISPUTE_SETTLED: 'settlement:dispute'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class EscrowHooks {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.handlers = new Map(); // event -> Set of handlers
|
|
24
|
+
this.logger = options.logger || console;
|
|
25
|
+
this.continueOnError = options.continueOnError !== false; // default true
|
|
26
|
+
|
|
27
|
+
// Initialize event handler sets
|
|
28
|
+
for (const event of Object.values(EscrowEvent)) {
|
|
29
|
+
this.handlers.set(event, new Set());
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register a handler for an escrow event
|
|
35
|
+
* @param {string} event - Event name from EscrowEvent
|
|
36
|
+
* @param {Function} handler - Async function(payload) to call
|
|
37
|
+
* @returns {Function} Unsubscribe function
|
|
38
|
+
*/
|
|
39
|
+
on(event, handler) {
|
|
40
|
+
if (!this.handlers.has(event)) {
|
|
41
|
+
throw new Error(`Unknown escrow event: ${event}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof handler !== 'function') {
|
|
45
|
+
throw new Error('Handler must be a function');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.handlers.get(event).add(handler);
|
|
49
|
+
|
|
50
|
+
// Return unsubscribe function
|
|
51
|
+
return () => this.off(event, handler);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Remove a handler for an escrow event
|
|
56
|
+
* @param {string} event - Event name
|
|
57
|
+
* @param {Function} handler - Handler to remove
|
|
58
|
+
*/
|
|
59
|
+
off(event, handler) {
|
|
60
|
+
if (this.handlers.has(event)) {
|
|
61
|
+
this.handlers.get(event).delete(handler);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Remove all handlers for an event (or all events)
|
|
67
|
+
* @param {string} [event] - Optional event name
|
|
68
|
+
*/
|
|
69
|
+
clear(event) {
|
|
70
|
+
if (event) {
|
|
71
|
+
if (this.handlers.has(event)) {
|
|
72
|
+
this.handlers.get(event).clear();
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
for (const handlers of this.handlers.values()) {
|
|
76
|
+
handlers.clear();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Emit an escrow event to all registered handlers
|
|
83
|
+
* @param {string} event - Event name
|
|
84
|
+
* @param {Object} payload - Event payload
|
|
85
|
+
* @returns {Promise<Object>} Results from all handlers
|
|
86
|
+
*/
|
|
87
|
+
async emit(event, payload) {
|
|
88
|
+
if (!this.handlers.has(event)) {
|
|
89
|
+
throw new Error(`Unknown escrow event: ${event}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const handlers = this.handlers.get(event);
|
|
93
|
+
if (handlers.size === 0) {
|
|
94
|
+
return { event, handled: false, results: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const results = [];
|
|
98
|
+
const errors = [];
|
|
99
|
+
|
|
100
|
+
for (const handler of handlers) {
|
|
101
|
+
try {
|
|
102
|
+
const result = await handler(payload);
|
|
103
|
+
results.push({ success: true, result });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const errorInfo = {
|
|
106
|
+
success: false,
|
|
107
|
+
error: err.message,
|
|
108
|
+
stack: err.stack
|
|
109
|
+
};
|
|
110
|
+
errors.push(errorInfo);
|
|
111
|
+
results.push(errorInfo);
|
|
112
|
+
|
|
113
|
+
this.logger.error?.(`[EscrowHooks] Error in ${event} handler:`, err.message);
|
|
114
|
+
|
|
115
|
+
if (!this.continueOnError) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
event,
|
|
123
|
+
handled: true,
|
|
124
|
+
results,
|
|
125
|
+
errors: errors.length > 0 ? errors : undefined
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if any handlers are registered for an event
|
|
131
|
+
* @param {string} event - Event name
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
hasHandlers(event) {
|
|
135
|
+
return this.handlers.has(event) && this.handlers.get(event).size > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get count of handlers for an event
|
|
140
|
+
* @param {string} event - Event name
|
|
141
|
+
* @returns {number}
|
|
142
|
+
*/
|
|
143
|
+
handlerCount(event) {
|
|
144
|
+
return this.handlers.has(event) ? this.handlers.get(event).size : 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create payload for escrow:created event
|
|
150
|
+
*/
|
|
151
|
+
export function createEscrowCreatedPayload(proposal, escrowResult) {
|
|
152
|
+
return {
|
|
153
|
+
event: EscrowEvent.CREATED,
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
proposal_id: proposal.id,
|
|
156
|
+
from_agent: proposal.from,
|
|
157
|
+
to_agent: proposal.to,
|
|
158
|
+
proposer_stake: proposal.proposer_stake || 0,
|
|
159
|
+
acceptor_stake: proposal.acceptor_stake || 0,
|
|
160
|
+
total_stake: (proposal.proposer_stake || 0) + (proposal.acceptor_stake || 0),
|
|
161
|
+
task: proposal.task,
|
|
162
|
+
amount: proposal.amount,
|
|
163
|
+
currency: proposal.currency,
|
|
164
|
+
expires: proposal.expires,
|
|
165
|
+
escrow_id: escrowResult.escrow?.proposal_id || proposal.id
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create payload for settlement:completion event
|
|
171
|
+
*/
|
|
172
|
+
export function createCompletionPayload(proposal, ratingChanges) {
|
|
173
|
+
const escrowInfo = ratingChanges?._escrow || {};
|
|
174
|
+
return {
|
|
175
|
+
event: EscrowEvent.COMPLETION_SETTLED,
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
proposal_id: proposal.id,
|
|
178
|
+
from_agent: proposal.from,
|
|
179
|
+
to_agent: proposal.to,
|
|
180
|
+
completed_by: proposal.completed_by,
|
|
181
|
+
completion_proof: proposal.completion_proof,
|
|
182
|
+
settlement: 'returned',
|
|
183
|
+
stakes_returned: {
|
|
184
|
+
proposer: escrowInfo.proposer_stake || 0,
|
|
185
|
+
acceptor: escrowInfo.acceptor_stake || 0
|
|
186
|
+
},
|
|
187
|
+
rating_changes: {
|
|
188
|
+
[proposal.from]: ratingChanges?.[proposal.from],
|
|
189
|
+
[proposal.to]: ratingChanges?.[proposal.to]
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create payload for settlement:dispute event
|
|
196
|
+
*/
|
|
197
|
+
export function createDisputePayload(proposal, ratingChanges) {
|
|
198
|
+
const escrowInfo = ratingChanges?._escrow || {};
|
|
199
|
+
return {
|
|
200
|
+
event: EscrowEvent.DISPUTE_SETTLED,
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
proposal_id: proposal.id,
|
|
203
|
+
from_agent: proposal.from,
|
|
204
|
+
to_agent: proposal.to,
|
|
205
|
+
disputed_by: proposal.disputed_by,
|
|
206
|
+
dispute_reason: proposal.dispute_reason,
|
|
207
|
+
settlement: escrowInfo.settlement || 'settled',
|
|
208
|
+
settlement_reason: escrowInfo.settlement_reason,
|
|
209
|
+
fault_determination: escrowInfo.fault_party,
|
|
210
|
+
stakes_transferred: escrowInfo.transferred,
|
|
211
|
+
stakes_burned: escrowInfo.burned,
|
|
212
|
+
rating_changes: {
|
|
213
|
+
[proposal.from]: ratingChanges?.[proposal.from],
|
|
214
|
+
[proposal.to]: ratingChanges?.[proposal.to]
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create payload for escrow:released event
|
|
221
|
+
*/
|
|
222
|
+
export function createEscrowReleasedPayload(proposalId, escrow, reason) {
|
|
223
|
+
return {
|
|
224
|
+
event: EscrowEvent.RELEASED,
|
|
225
|
+
timestamp: Date.now(),
|
|
226
|
+
proposal_id: proposalId,
|
|
227
|
+
from_agent: escrow.from?.agent_id,
|
|
228
|
+
to_agent: escrow.to?.agent_id,
|
|
229
|
+
stakes_released: {
|
|
230
|
+
proposer: escrow.from?.stake || 0,
|
|
231
|
+
acceptor: escrow.to?.stake || 0
|
|
232
|
+
},
|
|
233
|
+
reason: reason || 'expired'
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default EscrowHooks;
|
package/lib/identity.js
CHANGED
|
@@ -44,6 +44,7 @@ export class Identity {
|
|
|
44
44
|
this.pubkey = data.pubkey; // PEM format
|
|
45
45
|
this.privkey = data.privkey; // PEM format (null if loaded from export)
|
|
46
46
|
this.created = data.created;
|
|
47
|
+
this.rotations = data.rotations || []; // Array of rotation records
|
|
47
48
|
|
|
48
49
|
// Lazy-load crypto key objects
|
|
49
50
|
this._publicKey = null;
|
|
@@ -85,7 +86,8 @@ export class Identity {
|
|
|
85
86
|
name: this.name,
|
|
86
87
|
pubkey: this.pubkey,
|
|
87
88
|
privkey: this.privkey,
|
|
88
|
-
created: this.created
|
|
89
|
+
created: this.created,
|
|
90
|
+
rotations: this.rotations
|
|
89
91
|
};
|
|
90
92
|
|
|
91
93
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2), {
|
|
@@ -160,7 +162,137 @@ export class Identity {
|
|
|
160
162
|
return {
|
|
161
163
|
name: this.name,
|
|
162
164
|
pubkey: this.pubkey,
|
|
163
|
-
created: this.created
|
|
165
|
+
created: this.created,
|
|
166
|
+
rotations: this.rotations
|
|
164
167
|
};
|
|
165
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Rotate to a new keypair
|
|
172
|
+
* Signs the new public key with the old private key for chain of custody
|
|
173
|
+
* @returns {object} Rotation record with old_pubkey, new_pubkey, signature, timestamp
|
|
174
|
+
*/
|
|
175
|
+
rotate() {
|
|
176
|
+
if (!this.privkey) {
|
|
177
|
+
throw new Error('Private key not available - cannot rotate');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Generate new keypair
|
|
181
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
182
|
+
const newPubkey = publicKey.export({ type: 'spki', format: 'pem' });
|
|
183
|
+
const newPrivkey = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
184
|
+
|
|
185
|
+
// Use same timestamp for both signing and record
|
|
186
|
+
const timestamp = new Date().toISOString();
|
|
187
|
+
|
|
188
|
+
// Create rotation record content to sign
|
|
189
|
+
const rotationContent = JSON.stringify({
|
|
190
|
+
old_pubkey: this.pubkey,
|
|
191
|
+
new_pubkey: newPubkey,
|
|
192
|
+
timestamp
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Sign with old private key
|
|
196
|
+
const signature = this.sign(rotationContent);
|
|
197
|
+
|
|
198
|
+
// Create rotation record
|
|
199
|
+
const rotationRecord = {
|
|
200
|
+
old_pubkey: this.pubkey,
|
|
201
|
+
old_agent_id: this.getAgentId(),
|
|
202
|
+
new_pubkey: newPubkey,
|
|
203
|
+
new_agent_id: pubkeyToAgentId(newPubkey),
|
|
204
|
+
signature,
|
|
205
|
+
timestamp
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Update identity with new keys
|
|
209
|
+
this.rotations.push(rotationRecord);
|
|
210
|
+
this.pubkey = newPubkey;
|
|
211
|
+
this.privkey = newPrivkey;
|
|
212
|
+
this._publicKey = null;
|
|
213
|
+
this._privateKey = null;
|
|
214
|
+
|
|
215
|
+
return rotationRecord;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Verify a rotation record
|
|
220
|
+
* Checks that the signature is valid using the old public key
|
|
221
|
+
* @param {object} record - Rotation record to verify
|
|
222
|
+
* @returns {boolean} True if signature is valid
|
|
223
|
+
*/
|
|
224
|
+
static verifyRotation(record) {
|
|
225
|
+
try {
|
|
226
|
+
const rotationContent = JSON.stringify({
|
|
227
|
+
old_pubkey: record.old_pubkey,
|
|
228
|
+
new_pubkey: record.new_pubkey,
|
|
229
|
+
timestamp: record.timestamp
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return Identity.verify(rotationContent, record.signature, record.old_pubkey);
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Verify the entire rotation chain
|
|
240
|
+
* @returns {object} { valid: boolean, errors: string[] }
|
|
241
|
+
*/
|
|
242
|
+
verifyRotationChain() {
|
|
243
|
+
const errors = [];
|
|
244
|
+
|
|
245
|
+
if (this.rotations.length === 0) {
|
|
246
|
+
return { valid: true, errors: [] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Verify each rotation in sequence
|
|
250
|
+
for (let i = 0; i < this.rotations.length; i++) {
|
|
251
|
+
const record = this.rotations[i];
|
|
252
|
+
|
|
253
|
+
// Verify signature
|
|
254
|
+
if (!Identity.verifyRotation(record)) {
|
|
255
|
+
errors.push(`Rotation ${i + 1}: Invalid signature`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Verify chain continuity (each new_pubkey should match next old_pubkey)
|
|
260
|
+
if (i < this.rotations.length - 1) {
|
|
261
|
+
const nextRecord = this.rotations[i + 1];
|
|
262
|
+
if (record.new_pubkey !== nextRecord.old_pubkey) {
|
|
263
|
+
errors.push(`Rotation ${i + 1}: Chain break - new_pubkey doesn't match next old_pubkey`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Verify final pubkey matches current identity
|
|
269
|
+
const lastRotation = this.rotations[this.rotations.length - 1];
|
|
270
|
+
if (lastRotation.new_pubkey !== this.pubkey) {
|
|
271
|
+
errors.push('Final rotation new_pubkey does not match current identity pubkey');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
valid: errors.length === 0,
|
|
276
|
+
errors
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get the original (genesis) public key before any rotations
|
|
282
|
+
* @returns {string} Original public key in PEM format
|
|
283
|
+
*/
|
|
284
|
+
getOriginalPubkey() {
|
|
285
|
+
if (this.rotations.length === 0) {
|
|
286
|
+
return this.pubkey;
|
|
287
|
+
}
|
|
288
|
+
return this.rotations[0].old_pubkey;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get the original (genesis) agent ID
|
|
293
|
+
* @returns {string} Original agent ID
|
|
294
|
+
*/
|
|
295
|
+
getOriginalAgentId() {
|
|
296
|
+
return pubkeyToAgentId(this.getOriginalPubkey());
|
|
297
|
+
}
|
|
166
298
|
}
|
package/lib/proposals.js
CHANGED
|
@@ -45,6 +45,10 @@ export class ProposalStore {
|
|
|
45
45
|
created_at: now,
|
|
46
46
|
updated_at: now,
|
|
47
47
|
sig: proposal.sig,
|
|
48
|
+
// ELO staking
|
|
49
|
+
proposer_stake: proposal.elo_stake || null,
|
|
50
|
+
acceptor_stake: null,
|
|
51
|
+
stakes_escrowed: false,
|
|
48
52
|
// Response tracking
|
|
49
53
|
response_sig: null,
|
|
50
54
|
response_payment_code: null,
|
|
@@ -82,8 +86,13 @@ export class ProposalStore {
|
|
|
82
86
|
|
|
83
87
|
/**
|
|
84
88
|
* Accept a proposal
|
|
89
|
+
* @param {string} id - Proposal ID
|
|
90
|
+
* @param {string} acceptorId - Agent accepting the proposal
|
|
91
|
+
* @param {string} sig - Signature of acceptance
|
|
92
|
+
* @param {string} payment_code - Optional payment code
|
|
93
|
+
* @param {number} acceptor_stake - Optional ELO stake from acceptor
|
|
85
94
|
*/
|
|
86
|
-
accept(id, acceptorId, sig, payment_code = null) {
|
|
95
|
+
accept(id, acceptorId, sig, payment_code = null, acceptor_stake = null) {
|
|
87
96
|
const proposal = this.get(id);
|
|
88
97
|
if (!proposal) {
|
|
89
98
|
return { error: 'PROPOSAL_NOT_FOUND' };
|
|
@@ -106,6 +115,7 @@ export class ProposalStore {
|
|
|
106
115
|
proposal.status = ProposalStatus.ACCEPTED;
|
|
107
116
|
proposal.response_sig = sig;
|
|
108
117
|
proposal.response_payment_code = payment_code;
|
|
118
|
+
proposal.acceptor_stake = acceptor_stake;
|
|
109
119
|
proposal.updated_at = Date.now();
|
|
110
120
|
|
|
111
121
|
return { proposal };
|
|
@@ -296,7 +306,8 @@ export function formatProposal(proposal) {
|
|
|
296
306
|
expires: proposal.expires,
|
|
297
307
|
status: proposal.status,
|
|
298
308
|
created_at: proposal.created_at,
|
|
299
|
-
sig: proposal.sig
|
|
309
|
+
sig: proposal.sig,
|
|
310
|
+
elo_stake: proposal.proposer_stake
|
|
300
311
|
};
|
|
301
312
|
}
|
|
302
313
|
|
|
@@ -317,7 +328,9 @@ export function formatProposalResponse(proposal, responseType) {
|
|
|
317
328
|
from: proposal.from,
|
|
318
329
|
to: proposal.to,
|
|
319
330
|
payment_code: proposal.response_payment_code,
|
|
320
|
-
sig: proposal.response_sig
|
|
331
|
+
sig: proposal.response_sig,
|
|
332
|
+
proposer_stake: proposal.proposer_stake,
|
|
333
|
+
acceptor_stake: proposal.acceptor_stake
|
|
321
334
|
};
|
|
322
335
|
|
|
323
336
|
case 'reject':
|
|
@@ -337,7 +350,11 @@ export function formatProposalResponse(proposal, responseType) {
|
|
|
337
350
|
completed_by: proposal.completed_by,
|
|
338
351
|
completed_at: proposal.completed_at,
|
|
339
352
|
proof: proposal.completion_proof,
|
|
340
|
-
sig: proposal.completion_sig
|
|
353
|
+
sig: proposal.completion_sig,
|
|
354
|
+
elo_stakes: {
|
|
355
|
+
proposer: proposal.proposer_stake || 0,
|
|
356
|
+
acceptor: proposal.acceptor_stake || 0
|
|
357
|
+
}
|
|
341
358
|
};
|
|
342
359
|
|
|
343
360
|
case 'dispute':
|
|
@@ -348,7 +365,11 @@ export function formatProposalResponse(proposal, responseType) {
|
|
|
348
365
|
disputed_by: proposal.disputed_by,
|
|
349
366
|
disputed_at: proposal.disputed_at,
|
|
350
367
|
reason: proposal.dispute_reason,
|
|
351
|
-
sig: proposal.dispute_sig
|
|
368
|
+
sig: proposal.dispute_sig,
|
|
369
|
+
elo_stakes: {
|
|
370
|
+
proposer: proposal.proposer_stake || 0,
|
|
371
|
+
acceptor: proposal.acceptor_stake || 0
|
|
372
|
+
}
|
|
352
373
|
};
|
|
353
374
|
|
|
354
375
|
default:
|
|
@@ -367,16 +388,20 @@ export function getProposalSigningContent(proposal) {
|
|
|
367
388
|
proposal.amount || '',
|
|
368
389
|
proposal.currency || '',
|
|
369
390
|
proposal.payment_code || '',
|
|
370
|
-
proposal.expires || ''
|
|
391
|
+
proposal.expires || '',
|
|
392
|
+
proposal.elo_stake || ''
|
|
371
393
|
];
|
|
372
394
|
return fields.join('|');
|
|
373
395
|
}
|
|
374
396
|
|
|
375
397
|
/**
|
|
376
398
|
* Create accept content string for signing
|
|
399
|
+
* @param {string} proposalId - The proposal being accepted
|
|
400
|
+
* @param {string} payment_code - Optional payment code
|
|
401
|
+
* @param {number} elo_stake - Optional ELO stake from acceptor
|
|
377
402
|
*/
|
|
378
|
-
export function getAcceptSigningContent(proposalId, payment_code = '') {
|
|
379
|
-
return `ACCEPT|${proposalId}|${payment_code}`;
|
|
403
|
+
export function getAcceptSigningContent(proposalId, payment_code = '', elo_stake = '') {
|
|
404
|
+
return `ACCEPT|${proposalId}|${payment_code}|${elo_stake}`;
|
|
380
405
|
}
|
|
381
406
|
|
|
382
407
|
/**
|
package/lib/protocol.js
CHANGED
|
@@ -24,7 +24,12 @@ export const ClientMessageType = {
|
|
|
24
24
|
DISPUTE: 'DISPUTE',
|
|
25
25
|
// Skill discovery message types
|
|
26
26
|
REGISTER_SKILLS: 'REGISTER_SKILLS',
|
|
27
|
-
SEARCH_SKILLS: 'SEARCH_SKILLS'
|
|
27
|
+
SEARCH_SKILLS: 'SEARCH_SKILLS',
|
|
28
|
+
// Presence message types
|
|
29
|
+
SET_PRESENCE: 'SET_PRESENCE',
|
|
30
|
+
// Identity verification message types
|
|
31
|
+
VERIFY_REQUEST: 'VERIFY_REQUEST',
|
|
32
|
+
VERIFY_RESPONSE: 'VERIFY_RESPONSE'
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
// Server -> Client message types
|
|
@@ -47,7 +52,14 @@ export const ServerMessageType = {
|
|
|
47
52
|
DISPUTE: 'DISPUTE',
|
|
48
53
|
// Skill discovery message types
|
|
49
54
|
SKILLS_REGISTERED: 'SKILLS_REGISTERED',
|
|
50
|
-
SEARCH_RESULTS: 'SEARCH_RESULTS'
|
|
55
|
+
SEARCH_RESULTS: 'SEARCH_RESULTS',
|
|
56
|
+
// Presence message types
|
|
57
|
+
PRESENCE_CHANGED: 'PRESENCE_CHANGED',
|
|
58
|
+
// Identity verification message types
|
|
59
|
+
VERIFY_REQUEST: 'VERIFY_REQUEST',
|
|
60
|
+
VERIFY_RESPONSE: 'VERIFY_RESPONSE',
|
|
61
|
+
VERIFY_SUCCESS: 'VERIFY_SUCCESS',
|
|
62
|
+
VERIFY_FAILED: 'VERIFY_FAILED'
|
|
51
63
|
};
|
|
52
64
|
|
|
53
65
|
// Error codes
|
|
@@ -65,7 +77,22 @@ export const ErrorCode = {
|
|
|
65
77
|
PROPOSAL_EXPIRED: 'PROPOSAL_EXPIRED',
|
|
66
78
|
INVALID_PROPOSAL: 'INVALID_PROPOSAL',
|
|
67
79
|
SIGNATURE_REQUIRED: 'SIGNATURE_REQUIRED',
|
|
68
|
-
NOT_PROPOSAL_PARTY: 'NOT_PROPOSAL_PARTY'
|
|
80
|
+
NOT_PROPOSAL_PARTY: 'NOT_PROPOSAL_PARTY',
|
|
81
|
+
// Staking errors
|
|
82
|
+
INSUFFICIENT_REPUTATION: 'INSUFFICIENT_REPUTATION',
|
|
83
|
+
INVALID_STAKE: 'INVALID_STAKE',
|
|
84
|
+
// Verification errors
|
|
85
|
+
VERIFICATION_FAILED: 'VERIFICATION_FAILED',
|
|
86
|
+
VERIFICATION_EXPIRED: 'VERIFICATION_EXPIRED',
|
|
87
|
+
NO_PUBKEY: 'NO_PUBKEY'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Presence status
|
|
91
|
+
export const PresenceStatus = {
|
|
92
|
+
ONLINE: 'online',
|
|
93
|
+
AWAY: 'away',
|
|
94
|
+
BUSY: 'busy',
|
|
95
|
+
OFFLINE: 'offline'
|
|
69
96
|
};
|
|
70
97
|
|
|
71
98
|
// Proposal status
|
|
@@ -250,20 +277,40 @@ export function validateClientMessage(raw) {
|
|
|
250
277
|
if (!msg.sig) {
|
|
251
278
|
return { valid: false, error: 'Proposals must be signed' };
|
|
252
279
|
}
|
|
253
|
-
// Optional fields: amount, currency, payment_code, expires, terms
|
|
280
|
+
// Optional fields: amount, currency, payment_code, expires, terms, elo_stake
|
|
254
281
|
if (msg.expires !== undefined && typeof msg.expires !== 'number') {
|
|
255
282
|
return { valid: false, error: 'expires must be a number (seconds)' };
|
|
256
283
|
}
|
|
284
|
+
if (msg.elo_stake !== undefined) {
|
|
285
|
+
if (typeof msg.elo_stake !== 'number' || msg.elo_stake < 0 || !Number.isInteger(msg.elo_stake)) {
|
|
286
|
+
return { valid: false, error: 'elo_stake must be a non-negative integer' };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
257
289
|
break;
|
|
258
290
|
|
|
259
291
|
case ClientMessageType.ACCEPT:
|
|
292
|
+
// Accept requires: proposal_id and signature
|
|
293
|
+
if (!msg.proposal_id) {
|
|
294
|
+
return { valid: false, error: 'Missing proposal_id' };
|
|
295
|
+
}
|
|
296
|
+
if (!msg.sig) {
|
|
297
|
+
return { valid: false, error: 'Accept must be signed' };
|
|
298
|
+
}
|
|
299
|
+
// Optional: elo_stake for acceptor's stake
|
|
300
|
+
if (msg.elo_stake !== undefined) {
|
|
301
|
+
if (typeof msg.elo_stake !== 'number' || msg.elo_stake < 0 || !Number.isInteger(msg.elo_stake)) {
|
|
302
|
+
return { valid: false, error: 'elo_stake must be a non-negative integer' };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
|
|
260
307
|
case ClientMessageType.REJECT:
|
|
261
|
-
//
|
|
308
|
+
// Reject requires: proposal_id and signature
|
|
262
309
|
if (!msg.proposal_id) {
|
|
263
310
|
return { valid: false, error: 'Missing proposal_id' };
|
|
264
311
|
}
|
|
265
312
|
if (!msg.sig) {
|
|
266
|
-
return { valid: false, error: '
|
|
313
|
+
return { valid: false, error: 'Reject must be signed' };
|
|
267
314
|
}
|
|
268
315
|
break;
|
|
269
316
|
|
|
@@ -317,6 +364,50 @@ export function validateClientMessage(raw) {
|
|
|
317
364
|
// query_id is optional but useful for tracking responses
|
|
318
365
|
break;
|
|
319
366
|
|
|
367
|
+
case ClientMessageType.SET_PRESENCE:
|
|
368
|
+
// Set presence requires: status (online, away, busy, offline)
|
|
369
|
+
const validStatuses = ['online', 'away', 'busy', 'offline'];
|
|
370
|
+
if (!msg.status || !validStatuses.includes(msg.status)) {
|
|
371
|
+
return { valid: false, error: `Invalid presence status. Must be one of: ${validStatuses.join(', ')}` };
|
|
372
|
+
}
|
|
373
|
+
// Optional: status_text for custom message
|
|
374
|
+
if (msg.status_text !== undefined && typeof msg.status_text !== 'string') {
|
|
375
|
+
return { valid: false, error: 'status_text must be a string' };
|
|
376
|
+
}
|
|
377
|
+
if (msg.status_text && msg.status_text.length > 100) {
|
|
378
|
+
return { valid: false, error: 'status_text too long (max 100 chars)' };
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case ClientMessageType.VERIFY_REQUEST:
|
|
383
|
+
// Verify request requires: target agent and nonce
|
|
384
|
+
if (!msg.target) {
|
|
385
|
+
return { valid: false, error: 'Missing target agent' };
|
|
386
|
+
}
|
|
387
|
+
if (!isAgent(msg.target)) {
|
|
388
|
+
return { valid: false, error: 'Target must be an agent (@id)' };
|
|
389
|
+
}
|
|
390
|
+
if (!msg.nonce || typeof msg.nonce !== 'string') {
|
|
391
|
+
return { valid: false, error: 'Missing or invalid nonce' };
|
|
392
|
+
}
|
|
393
|
+
if (msg.nonce.length < 16 || msg.nonce.length > 128) {
|
|
394
|
+
return { valid: false, error: 'Nonce must be 16-128 characters' };
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case ClientMessageType.VERIFY_RESPONSE:
|
|
399
|
+
// Verify response requires: request_id, nonce, and signature
|
|
400
|
+
if (!msg.request_id) {
|
|
401
|
+
return { valid: false, error: 'Missing request_id' };
|
|
402
|
+
}
|
|
403
|
+
if (!msg.nonce || typeof msg.nonce !== 'string') {
|
|
404
|
+
return { valid: false, error: 'Missing or invalid nonce' };
|
|
405
|
+
}
|
|
406
|
+
if (!msg.sig || typeof msg.sig !== 'string') {
|
|
407
|
+
return { valid: false, error: 'Missing or invalid signature' };
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
|
|
320
411
|
default:
|
|
321
412
|
return { valid: false, error: `Unknown message type: ${msg.type}` };
|
|
322
413
|
}
|
|
@@ -372,3 +463,21 @@ export function isProposalMessage(type) {
|
|
|
372
463
|
ClientMessageType.DISPUTE
|
|
373
464
|
].includes(type);
|
|
374
465
|
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Generate a unique verification request ID
|
|
469
|
+
* Format: verify_<timestamp>_<random>
|
|
470
|
+
*/
|
|
471
|
+
export function generateVerifyId() {
|
|
472
|
+
const timestamp = Date.now().toString(36);
|
|
473
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
474
|
+
return `verify_${timestamp}_${random}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Generate a random nonce for identity verification
|
|
479
|
+
* Returns a 32-character hex string
|
|
480
|
+
*/
|
|
481
|
+
export function generateNonce() {
|
|
482
|
+
return crypto.randomBytes(16).toString('hex');
|
|
483
|
+
}
|