agent-escrow 0.1.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/src/task.cjs ADDED
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { KINDS, TASK_STATUS, signEvent, publishToRelays, queryRelays, getTag, getTags } = require('./nostr.cjs');
5
+
6
+ /**
7
+ * Create a task event template
8
+ */
9
+ function createTaskEvent(opts) {
10
+ const {
11
+ title,
12
+ description,
13
+ budget,
14
+ deadline,
15
+ capabilities = [],
16
+ minTrust,
17
+ lightningAddress,
18
+ taskId
19
+ } = opts;
20
+
21
+ if (!title) throw new Error('title is required');
22
+ if (!budget || budget <= 0) throw new Error('budget must be positive');
23
+
24
+ const id = taskId || crypto.randomUUID();
25
+
26
+ const tags = [
27
+ ['d', id],
28
+ ['title', title],
29
+ ['budget', String(budget)],
30
+ ['status', TASK_STATUS.OPEN]
31
+ ];
32
+
33
+ if (deadline) tags.push(['deadline', String(Math.floor(deadline / 1000))]);
34
+ for (const cap of capabilities) tags.push(['c', cap]);
35
+ if (minTrust) tags.push(['min-trust', String(minTrust)]);
36
+ if (lightningAddress) tags.push(['ln', lightningAddress]);
37
+
38
+ return {
39
+ kind: KINDS.TASK,
40
+ created_at: Math.floor(Date.now() / 1000),
41
+ tags,
42
+ content: description || ''
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Create an updated task event (same d-tag, new status)
48
+ */
49
+ function updateTaskEvent(originalEvent, updates) {
50
+ const tags = originalEvent.tags.map(t => {
51
+ if (t[0] === 'status' && updates.status) return ['status', updates.status];
52
+ return [...t];
53
+ });
54
+
55
+ // Add worker pubkey on claim
56
+ if (updates.workerPubkey && !tags.find(t => t[0] === 'p')) {
57
+ tags.push(['p', updates.workerPubkey]);
58
+ }
59
+
60
+ return {
61
+ kind: KINDS.TASK,
62
+ created_at: Math.floor(Date.now() / 1000),
63
+ tags,
64
+ content: updates.content || originalEvent.content
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Parse a task event into a structured object
70
+ */
71
+ function parseTask(event) {
72
+ return {
73
+ eventId: event.id,
74
+ taskId: getTag(event, 'd'),
75
+ poster: event.pubkey,
76
+ title: getTag(event, 'title'),
77
+ description: event.content,
78
+ budget: parseInt(getTag(event, 'budget') || '0', 10),
79
+ deadline: getTag(event, 'deadline') ? parseInt(getTag(event, 'deadline'), 10) * 1000 : null,
80
+ status: getTag(event, 'status') || TASK_STATUS.OPEN,
81
+ capabilities: getTags(event, 'c'),
82
+ minTrust: getTag(event, 'min-trust') ? parseInt(getTag(event, 'min-trust'), 10) : null,
83
+ lightningAddress: getTag(event, 'ln'),
84
+ worker: getTag(event, 'p'),
85
+ createdAt: event.created_at * 1000,
86
+ raw: event
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Build filters to find tasks on relays
92
+ */
93
+ function buildTaskFilters(opts = {}) {
94
+ const filter = { kinds: [KINDS.TASK] };
95
+
96
+ if (opts.status) filter['#status'] = Array.isArray(opts.status) ? opts.status : [opts.status];
97
+ if (opts.capabilities) filter['#c'] = Array.isArray(opts.capabilities) ? opts.capabilities : [opts.capabilities];
98
+ if (opts.poster) filter.authors = Array.isArray(opts.poster) ? opts.poster : [opts.poster];
99
+ if (opts.limit) filter.limit = opts.limit;
100
+ if (opts.since) filter.since = Math.floor(opts.since / 1000);
101
+
102
+ return filter;
103
+ }
104
+
105
+ /**
106
+ * Query relays for tasks
107
+ */
108
+ async function findTasks(relays, opts = {}) {
109
+ const filter = buildTaskFilters(opts);
110
+ const events = await queryRelays(relays, filter, opts.timeoutMs);
111
+
112
+ let tasks = events.map(parseTask);
113
+
114
+ // Client-side filtering for things relays can't filter
115
+ if (opts.minBudget) tasks = tasks.filter(t => t.budget >= opts.minBudget);
116
+ if (opts.maxBudget) tasks = tasks.filter(t => t.budget <= opts.maxBudget);
117
+
118
+ // Sort by creation time, newest first
119
+ tasks.sort((a, b) => b.createdAt - a.createdAt);
120
+
121
+ return tasks;
122
+ }
123
+
124
+ /**
125
+ * Get a specific task by its d-tag (taskId)
126
+ */
127
+ async function getTask(relays, posterPubkey, taskId, timeoutMs = 8000) {
128
+ const filter = {
129
+ kinds: [KINDS.TASK],
130
+ authors: [posterPubkey],
131
+ '#d': [taskId]
132
+ };
133
+ const events = await queryRelays(relays, filter, timeoutMs);
134
+ if (events.length === 0) return null;
135
+
136
+ // Parameterized replaceable: latest event wins
137
+ events.sort((a, b) => b.created_at - a.created_at);
138
+ return parseTask(events[0]);
139
+ }
140
+
141
+ module.exports = {
142
+ createTaskEvent,
143
+ updateTaskEvent,
144
+ parseTask,
145
+ buildTaskFilters,
146
+ findTasks,
147
+ getTask
148
+ };
package/src/trust.cjs ADDED
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Trust integration — optional ai.wot dependency
5
+ *
6
+ * Falls back gracefully if ai-wot is not installed.
7
+ */
8
+
9
+ let aiWot = null;
10
+ try {
11
+ aiWot = require('ai-wot');
12
+ } catch (e) {
13
+ // ai-wot not installed — trust features disabled
14
+ }
15
+
16
+ /**
17
+ * Check if trust features are available
18
+ */
19
+ function trustAvailable() {
20
+ return aiWot !== null;
21
+ }
22
+
23
+ /**
24
+ * Get trust score for a pubkey
25
+ * Returns null if ai-wot is not installed
26
+ */
27
+ async function getTrustScore(pubkey, opts = {}) {
28
+ if (!aiWot) return null;
29
+
30
+ try {
31
+ const relayUrls = opts.relays || ['wss://relay.damus.io', 'wss://nos.lol'];
32
+ const score = await aiWot.calculateTrustScore(pubkey, { relayUrls });
33
+ return score;
34
+ } catch (e) {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Check if a pubkey meets minimum trust threshold
41
+ * Returns true if ai-wot is not installed (permissive default)
42
+ */
43
+ async function meetsTrustThreshold(pubkey, minScore, opts = {}) {
44
+ if (!aiWot) return true; // No trust system = trust everyone
45
+ if (!minScore || minScore <= 0) return true;
46
+
47
+ const score = await getTrustScore(pubkey, opts);
48
+ if (score === null) return true; // Can't check = allow
49
+ return score.total >= minScore;
50
+ }
51
+
52
+ /**
53
+ * Publish a work-completed attestation
54
+ * Returns null if ai-wot is not installed
55
+ */
56
+ async function publishWorkAttestation(opts = {}) {
57
+ if (!aiWot) return null;
58
+
59
+ const {
60
+ targetPubkey,
61
+ comment,
62
+ secretKey,
63
+ relayUrls = ['wss://relay.damus.io', 'wss://nos.lol']
64
+ } = opts;
65
+
66
+ if (!targetPubkey) throw new Error('targetPubkey is required');
67
+ if (!secretKey) throw new Error('secretKey is required');
68
+
69
+ try {
70
+ const attestation = await aiWot.publishAttestation({
71
+ targetPubkey,
72
+ type: 'work-completed',
73
+ comment: comment || 'Task completed via agent-escrow marketplace',
74
+ secretKey,
75
+ relayUrls
76
+ });
77
+ return attestation;
78
+ } catch (e) {
79
+ // Non-fatal — trust attestation is a bonus, not a requirement
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Publish a negative attestation (for disputes)
86
+ */
87
+ async function publishDisputeAttestation(opts = {}) {
88
+ if (!aiWot) return null;
89
+
90
+ const {
91
+ targetPubkey,
92
+ comment,
93
+ secretKey,
94
+ relayUrls = ['wss://relay.damus.io', 'wss://nos.lol']
95
+ } = opts;
96
+
97
+ if (!targetPubkey) throw new Error('targetPubkey is required');
98
+ if (!secretKey) throw new Error('secretKey is required');
99
+
100
+ try {
101
+ const attestation = await aiWot.publishAttestation({
102
+ targetPubkey,
103
+ type: 'service-quality',
104
+ comment: comment || 'Dispute filed via agent-escrow marketplace',
105
+ secretKey,
106
+ relayUrls
107
+ });
108
+ return attestation;
109
+ } catch (e) {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ module.exports = {
115
+ trustAvailable,
116
+ getTrustScore,
117
+ meetsTrustThreshold,
118
+ publishWorkAttestation,
119
+ publishDisputeAttestation
120
+ };