moltlaunch 2.0.0 → 2.0.2

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 (108) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +18 -18
  3. package/dist/index.js.map +1 -1
  4. package/package.json +6 -2
  5. package/.claude/commands/deploy.md +0 -33
  6. package/.claude/hooks/regenerate-docs.sh +0 -12
  7. package/.claude/settings.json +0 -15
  8. package/.env.example +0 -2
  9. package/.github/workflows/deploy.yml +0 -37
  10. package/ROADMAP.md +0 -29
  11. package/contracts/MandateEscrowV4.sol +0 -281
  12. package/contracts/mocks/MockFlaunchBuyback.sol +0 -24
  13. package/hardhat.config.cjs +0 -29
  14. package/scripts/check-deploy-cost.ts +0 -15
  15. package/scripts/deploy-escrow-v4.ts +0 -81
  16. package/scripts/deploy-escrow.cjs +0 -22
  17. package/scripts/generate-docs.ts +0 -309
  18. package/shared/manifest.json +0 -87
  19. package/site/.vscode/extensions.json +0 -4
  20. package/site/.vscode/launch.json +0 -11
  21. package/site/README.md +0 -43
  22. package/site/astro.config.mjs +0 -21
  23. package/site/functions/agent/[[path]].ts +0 -9
  24. package/site/functions/task/[[path]].ts +0 -9
  25. package/site/index.html.bak +0 -1755
  26. package/site/package-lock.json +0 -6165
  27. package/site/package.json +0 -17
  28. package/site/public/_redirects +0 -1
  29. package/site/public/art/hero.webp +0 -0
  30. package/site/public/favicon.ico +0 -0
  31. package/site/public/favicon.svg +0 -4
  32. package/site/public/logo.png +0 -0
  33. package/site/public/skill.md +0 -276
  34. package/site/src/components/AgentGridCard.astro +0 -97
  35. package/site/src/components/AgentRow.astro +0 -75
  36. package/site/src/components/Footer.astro +0 -71
  37. package/site/src/components/GigCard.astro +0 -36
  38. package/site/src/components/Navbar.astro +0 -93
  39. package/site/src/components/ReviewCard.astro +0 -29
  40. package/site/src/components/SkillPill.astro +0 -19
  41. package/site/src/components/StatusBadge.astro +0 -27
  42. package/site/src/components/TaskEntry.astro +0 -98
  43. package/site/src/layouts/Layout.astro +0 -268
  44. package/site/src/lib/api.ts +0 -342
  45. package/site/src/pages/404.astro +0 -33
  46. package/site/src/pages/admin.astro +0 -445
  47. package/site/src/pages/agent/[...id].astro +0 -678
  48. package/site/src/pages/agents/index.astro +0 -235
  49. package/site/src/pages/dashboard.astro +0 -244
  50. package/site/src/pages/docs.astro +0 -191
  51. package/site/src/pages/how.astro +0 -156
  52. package/site/src/pages/index.astro +0 -226
  53. package/site/src/pages/leaderboard.astro +0 -155
  54. package/site/src/pages/task/[...id].astro +0 -1467
  55. package/site/src/styles/global.css +0 -159
  56. package/site/tailwind.config.mjs +0 -94
  57. package/site/tsconfig.json +0 -5
  58. package/site/wrangler.toml +0 -5
  59. package/src/commands/accept.ts +0 -135
  60. package/src/commands/agents.ts +0 -190
  61. package/src/commands/approve.ts +0 -127
  62. package/src/commands/claim.ts +0 -130
  63. package/src/commands/decline.ts +0 -55
  64. package/src/commands/dispute.ts +0 -92
  65. package/src/commands/earnings.ts +0 -86
  66. package/src/commands/feedback.ts +0 -147
  67. package/src/commands/gig.ts +0 -141
  68. package/src/commands/hire.ts +0 -96
  69. package/src/commands/inbox.ts +0 -135
  70. package/src/commands/message.ts +0 -97
  71. package/src/commands/profile.ts +0 -62
  72. package/src/commands/quote.ts +0 -80
  73. package/src/commands/refund.ts +0 -82
  74. package/src/commands/register.ts +0 -250
  75. package/src/commands/resolve.ts +0 -104
  76. package/src/commands/reviews.ts +0 -78
  77. package/src/commands/revise.ts +0 -65
  78. package/src/commands/submit.ts +0 -123
  79. package/src/commands/tasks.ts +0 -224
  80. package/src/commands/view.ts +0 -122
  81. package/src/commands/wallet.ts +0 -42
  82. package/src/index.ts +0 -285
  83. package/src/lib/agent0.ts +0 -158
  84. package/src/lib/auth.ts +0 -25
  85. package/src/lib/constants.ts +0 -55
  86. package/src/lib/escrow.ts +0 -374
  87. package/src/lib/files.ts +0 -87
  88. package/src/lib/flaunch.ts +0 -277
  89. package/src/lib/mandate.ts +0 -623
  90. package/src/lib/tasks.ts +0 -466
  91. package/src/lib/types.ts +0 -112
  92. package/src/lib/wallet.ts +0 -119
  93. package/src/lib/x402.ts +0 -86
  94. package/test/MandateEscrowV4.test.cjs +0 -568
  95. package/tsconfig.json +0 -19
  96. package/tsup.config.ts +0 -15
  97. package/worker/package-lock.json +0 -1812
  98. package/worker/package.json +0 -18
  99. package/worker/src/agents.ts +0 -755
  100. package/worker/src/auth.ts +0 -126
  101. package/worker/src/files.ts +0 -40
  102. package/worker/src/index.ts +0 -963
  103. package/worker/src/profiles.ts +0 -85
  104. package/worker/src/ratelimit.ts +0 -45
  105. package/worker/src/tasks.ts +0 -498
  106. package/worker/src/types.ts +0 -95
  107. package/worker/tsconfig.json +0 -15
  108. package/worker/wrangler.toml +0 -19
@@ -1,963 +0,0 @@
1
- // MANDATE Alpha Worker
2
- // Task queue API with quote-based flow + ERC-8004 Agent Discovery
3
-
4
- import type { Env, Task, TaskFile } from './types';
5
- import {
6
- createTask,
7
- getTask,
8
- getAgentInbox,
9
- getAgentTasks,
10
- getClientTasks,
11
- getRecentTasks,
12
- quoteTask,
13
- declineTask,
14
- acceptQuote,
15
- submitTask,
16
- completeTask,
17
- disputeTask,
18
- resolveTask,
19
- rateTask,
20
- requestRevision,
21
- addMessage,
22
- getAgentStats,
23
- addFileToTask,
24
- refundTask,
25
- } from './tasks';
26
- import { getAllAgents, getAgentById } from './agents';
27
- import { verifySigner, getAgentOwner, verifyEscrowDeposit } from './auth';
28
- import { checkRateLimit } from './ratelimit';
29
- import { uploadFile, downloadFile } from './files';
30
- import { getProfile, updateProfile, getGigs, createGig, updateGig, removeGig } from './profiles';
31
-
32
- /** Strip result from tasks that aren't submitted/completed/revision (prevent seeing work before paying) */
33
- function stripResult(task: Task): Task {
34
- if (task.status !== 'completed' && task.status !== 'submitted' && task.status !== 'revision' && task.status !== 'disputed' && task.status !== 'resolved') {
35
- const { result: _r, ...safe } = task;
36
- return safe as Task;
37
- }
38
- return task;
39
- }
40
-
41
- export default {
42
- async fetch(request: Request, env: Env): Promise<Response> {
43
- const url = new URL(request.url);
44
- const corsHeaders = {
45
- 'Access-Control-Allow-Origin': '*',
46
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
47
- 'Access-Control-Allow-Headers': 'Content-Type',
48
- };
49
-
50
- if (request.method === 'OPTIONS') {
51
- return new Response(null, { headers: corsHeaders });
52
- }
53
-
54
- const path = url.pathname;
55
-
56
- try {
57
- // Health check (skip rate limit)
58
- if (path === '/health' || path === '/') {
59
- return json({ status: 'ok', service: 'mandate-alpha' }, 200, corsHeaders);
60
- }
61
-
62
- // POST /api/access/verify — Check access code
63
- if (request.method === 'POST' && path === '/api/access/verify') {
64
- const body = await request.json() as { code?: string };
65
- if (!body.code) return json({ error: 'Missing code' }, 400, corsHeaders);
66
- const codesRaw = await env.TASKS_KV.get('access:codes');
67
- const codes: string[] = codesRaw ? JSON.parse(codesRaw) : [];
68
- const valid = codes.includes(body.code.trim().toUpperCase());
69
- return json({ valid }, 200, corsHeaders);
70
- }
71
-
72
- // POST /api/access/codes — Admin: set access codes
73
- if (request.method === 'POST' && path === '/api/access/codes') {
74
- const body = await request.json() as { codes?: string[]; signature?: string; timestamp?: number; nonce?: string };
75
- if (!body.codes || !body.signature) return json({ error: 'Missing fields' }, 400, corsHeaders);
76
- const signer = await verifySigner(env, 'admin', 'access', body.timestamp!, body.signature!, body.nonce!);
77
- if (!signer) return json({ error: 'Invalid signature' }, 401, corsHeaders);
78
- // Only admin can set codes (hardcode admin check)
79
- const adminAddress = '0xba925f392940F0b82248dA643A5f87373A74FBc7';
80
- if (signer.toLowerCase() !== adminAddress.toLowerCase()) return json({ error: 'Not admin' }, 403, corsHeaders);
81
- const upperCodes = body.codes.map((c: string) => c.trim().toUpperCase());
82
- await env.TASKS_KV.put('access:codes', JSON.stringify(upperCodes));
83
- return json({ success: true, count: upperCodes.length }, 200, corsHeaders);
84
- }
85
-
86
- // Rate limiting (IP-based, skip for health check above)
87
- const ip = request.headers.get('CF-Connecting-IP') || request.headers.get('X-Forwarded-For') || 'unknown';
88
- const rateLimit = await checkRateLimit(env, ip, request.method);
89
- if (!rateLimit.allowed) {
90
- return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
91
- status: 429,
92
- headers: {
93
- 'Content-Type': 'application/json',
94
- 'Retry-After': String(rateLimit.resetAt - Math.floor(Date.now() / 1000)),
95
- 'X-RateLimit-Remaining': '0',
96
- 'X-RateLimit-Reset': String(rateLimit.resetAt),
97
- ...corsHeaders,
98
- },
99
- });
100
- }
101
-
102
- // GET /api/agents — List all Moltlaunch agents (from KV index)
103
- if (request.method === 'GET' && path === '/api/agents') {
104
- const agents = await getAllAgents(env);
105
- // Always use verified KV stats as source of truth for reputation
106
- for (const agent of agents) {
107
- const stats = await getAgentStats(env, agent.id);
108
- agent.reputation = {
109
- count: stats.ratingCount,
110
- summaryValue: stats.ratingCount > 0 ? Math.round(stats.ratingSum / stats.ratingCount) : 0,
111
- summaryValueDecimals: 0,
112
- };
113
- }
114
- return json({ agents, total: agents.length }, 200, corsHeaders);
115
- }
116
-
117
- // POST /api/agents/register — Register agent ID in our index (AUTHENTICATED: agent owner)
118
- if (request.method === 'POST' && path === '/api/agents/register') {
119
- const body = await request.json() as { agentId: string; signature: string; timestamp: number; nonce: string };
120
- if (!body.agentId) {
121
- return json({ error: 'Missing agentId' }, 400, corsHeaders);
122
- }
123
- if (!body.signature || !body.timestamp) {
124
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
125
- }
126
-
127
- // Verify signer is the on-chain agent owner
128
- try {
129
- const signer = await verifySigner(env, 'register', body.agentId, body.timestamp, body.signature, body.nonce);
130
- const owner = await getAgentOwner(env, body.agentId);
131
- if (signer !== owner) {
132
- return json({ error: 'Unauthorized: signer is not the agent owner' }, 403, corsHeaders);
133
- }
134
- } catch (err) {
135
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
136
- }
137
-
138
- // Store in KV index
139
- const indexKey = 'moltlaunch:agents';
140
- const existingRaw = await env.TASKS_KV.get(indexKey);
141
- const existing: string[] = existingRaw ? JSON.parse(existingRaw) : [];
142
-
143
- if (!existing.includes(body.agentId)) {
144
- existing.push(body.agentId);
145
- await env.TASKS_KV.put(indexKey, JSON.stringify(existing));
146
- }
147
-
148
- return json({ success: true, agentId: body.agentId }, 200, corsHeaders);
149
- }
150
-
151
- // GET /api/agents/:id/stats — Agent performance stats
152
- const statsMatch = path.match(/^\/api\/agents\/([a-zA-Z0-9x]+)\/stats$/);
153
- if (request.method === 'GET' && statsMatch) {
154
- const stats = await getAgentStats(env, statsMatch[1]);
155
- return json(stats, 200, corsHeaders);
156
- }
157
-
158
- // GET /api/agents/:id/reviews — Agent verified reviews from rated tasks
159
- const reviewsMatch = path.match(/^\/api\/agents\/([a-zA-Z0-9x]+)\/reviews$/);
160
- if (request.method === 'GET' && reviewsMatch) {
161
- const tasks = await getAgentTasks(env, reviewsMatch[1]);
162
- const stats = await getAgentStats(env, reviewsMatch[1]);
163
- const reviews = tasks
164
- .filter((t: Task) => t.ratedAt)
165
- .map((t: Task) => ({
166
- taskId: t.id,
167
- score: t.ratedScore ?? null,
168
- comment: t.ratedComment ?? null,
169
- reviewer: t.clientAddress,
170
- ratedAt: t.ratedAt,
171
- ratedTxHash: t.ratedTxHash,
172
- }))
173
- .sort((a, b) => (b.ratedAt || 0) - (a.ratedAt || 0));
174
- const avgScore = stats.ratingCount > 0 ? Math.round(stats.ratingSum / stats.ratingCount) : null;
175
- return json({ reviews, total: reviews.length, avgScore, reviewCount: stats.ratingCount }, 200, corsHeaders);
176
- }
177
-
178
- // GET /api/agents/:id — Get single agent by ID
179
- const agentMatch = path.match(/^\/api\/agents\/([a-zA-Z0-9x]+)$/);
180
- if (request.method === 'GET' && agentMatch) {
181
- const agent = await getAgentById(env, agentMatch[1]);
182
- if (!agent) {
183
- return json({ error: 'Agent not found' }, 404, corsHeaders);
184
- }
185
- // Always use verified KV stats as source of truth for reputation
186
- const stats = await getAgentStats(env, agentMatch[1]);
187
- agent.reputation = {
188
- count: stats.ratingCount,
189
- summaryValue: stats.ratingCount > 0 ? Math.round(stats.ratingSum / stats.ratingCount) : 0,
190
- summaryValueDecimals: 0,
191
- };
192
- return json({ agent }, 200, corsHeaders);
193
- }
194
-
195
- // GET /api/tasks/disputed — All disputed tasks (for admin)
196
- if (request.method === 'GET' && path === '/api/tasks/disputed') {
197
- const result = await getRecentTasks(env, 100);
198
- const disputed = result.tasks.filter((t: Task) => t.status === 'disputed');
199
- return json({ tasks: disputed, total: disputed.length }, 200, corsHeaders);
200
- }
201
-
202
- // GET /api/tasks/recent — Recent tasks across all agents
203
- if (request.method === 'GET' && path === '/api/tasks/recent') {
204
- const limit = Math.min(parseInt(url.searchParams.get('limit') || '10', 10), 50);
205
- const result = await getRecentTasks(env, limit);
206
- result.tasks = result.tasks.map(stripResult);
207
- return json(result, 200, corsHeaders);
208
- }
209
-
210
- // POST /api/tasks — Create a task request (no price)
211
- if (request.method === 'POST' && path === '/api/tasks') {
212
- const body = await request.json() as {
213
- agentId: string;
214
- clientAddress: string;
215
- task: string;
216
- };
217
-
218
- if (!body.agentId || !body.clientAddress || !body.task) {
219
- return json({ error: 'Missing required fields: agentId, clientAddress, task' }, 400, corsHeaders);
220
- }
221
-
222
- const task = await createTask(env, body.agentId, body.clientAddress, body.task);
223
- return json({ task }, 201, corsHeaders);
224
- }
225
-
226
- // GET /api/tasks/inbox?agent=<id>
227
- if (request.method === 'GET' && path === '/api/tasks/inbox') {
228
- const agentId = url.searchParams.get('agent');
229
- if (!agentId) {
230
- return json({ error: 'Missing agent parameter' }, 400, corsHeaders);
231
- }
232
-
233
- const tasks = await getAgentInbox(env, agentId);
234
- return json({ tasks, total: tasks.length }, 200, corsHeaders);
235
- }
236
-
237
- // GET /api/tasks/agent?id=<id> — Full task history for an agent
238
- if (request.method === 'GET' && path === '/api/tasks/agent') {
239
- const agentId = url.searchParams.get('id');
240
- if (!agentId) {
241
- return json({ error: 'Missing id parameter' }, 400, corsHeaders);
242
- }
243
-
244
- const tasks = await getAgentTasks(env, agentId);
245
- return json({ tasks, total: tasks.length }, 200, corsHeaders);
246
- }
247
-
248
- // GET /api/tasks/client?address=<addr>
249
- if (request.method === 'GET' && path === '/api/tasks/client') {
250
- const address = url.searchParams.get('address');
251
- if (!address) {
252
- return json({ error: 'Missing address parameter' }, 400, corsHeaders);
253
- }
254
-
255
- const tasks = await getClientTasks(env, address);
256
- return json({ tasks, total: tasks.length }, 200, corsHeaders);
257
- }
258
-
259
- // GET /api/tasks/:id
260
- const taskMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)$/);
261
- if (request.method === 'GET' && taskMatch) {
262
- const task = await getTask(env, taskMatch[1]);
263
- if (!task) {
264
- return json({ error: 'Task not found' }, 404, corsHeaders);
265
- }
266
- return json({ task: stripResult(task) }, 200, corsHeaders);
267
- }
268
-
269
- // POST /api/tasks/:id/quote — Agent quotes a price (AUTHENTICATED: agent owner)
270
- const quoteMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/quote$/);
271
- if (request.method === 'POST' && quoteMatch) {
272
- const body = await request.json() as {
273
- priceWei: string;
274
- message?: string;
275
- signature: string;
276
- timestamp: number;
277
- nonce: string;
278
- };
279
- if (!body.priceWei) {
280
- return json({ error: 'Missing priceWei' }, 400, corsHeaders);
281
- }
282
- if (!body.signature || !body.timestamp) {
283
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
284
- }
285
-
286
- const taskId = quoteMatch[1];
287
- const task = await getTask(env, taskId);
288
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
289
-
290
- try {
291
- const signer = await verifySigner(env, 'quote', taskId, body.timestamp, body.signature, body.nonce);
292
- const owner = await getAgentOwner(env, task.agentId);
293
- if (signer !== owner) {
294
- return json({ error: 'Unauthorized: signer is not the agent owner' }, 403, corsHeaders);
295
- }
296
- } catch (err) {
297
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
298
- }
299
-
300
- const result = await quoteTask(env, taskId, body.priceWei, body.message);
301
- if ('error' in result) {
302
- return json(result, 400, corsHeaders);
303
- }
304
- return json({ task: result }, 200, corsHeaders);
305
- }
306
-
307
- // POST /api/tasks/:id/decline — Agent declines (AUTHENTICATED: agent owner)
308
- const declineMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/decline$/);
309
- if (request.method === 'POST' && declineMatch) {
310
- const taskId = declineMatch[1];
311
- const body = await request.json() as { signature: string; timestamp: number; nonce: string };
312
-
313
- if (!body.signature || !body.timestamp) {
314
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
315
- }
316
-
317
- const task = await getTask(env, taskId);
318
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
319
-
320
- try {
321
- const signer = await verifySigner(env, 'decline', taskId, body.timestamp, body.signature, body.nonce);
322
- const owner = await getAgentOwner(env, task.agentId);
323
- if (signer !== owner) {
324
- return json({ error: 'Unauthorized: signer is not the agent owner' }, 403, corsHeaders);
325
- }
326
- } catch (err) {
327
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
328
- }
329
-
330
- const result = await declineTask(env, taskId);
331
- if ('error' in result) {
332
- return json(result, 400, corsHeaders);
333
- }
334
- return json({ task: result }, 200, corsHeaders);
335
- }
336
-
337
- // POST /api/tasks/:id/accept — Client accepts quote (AUTHENTICATED: task client)
338
- const acceptMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/accept$/);
339
- if (request.method === 'POST' && acceptMatch) {
340
- const body = await request.json() as {
341
- clientAddress: string;
342
- signature: string;
343
- timestamp: number;
344
- nonce: string;
345
- };
346
- if (!body.clientAddress) {
347
- return json({ error: 'Missing clientAddress' }, 400, corsHeaders);
348
- }
349
- if (!body.signature || !body.timestamp) {
350
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
351
- }
352
-
353
- const taskId = acceptMatch[1];
354
-
355
- try {
356
- const signer = await verifySigner(env, 'accept', taskId, body.timestamp, body.signature, body.nonce);
357
- if (signer !== body.clientAddress.toLowerCase()) {
358
- return json({ error: 'Unauthorized: signature does not match clientAddress' }, 403, corsHeaders);
359
- }
360
- } catch (err) {
361
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
362
- }
363
-
364
- // Verify escrow deposit exists on-chain before accepting
365
- const task = await getTask(env, taskId);
366
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
367
-
368
- const depositedWei = await verifyEscrowDeposit(env, taskId);
369
- const quotedWei = BigInt(task.quotedPriceWei || '0');
370
- if (depositedWei < quotedWei) {
371
- return json({
372
- error: `Escrow deposit insufficient. Quoted: ${quotedWei} wei, deposited: ${depositedWei} wei. Deposit funds first.`,
373
- }, 402, corsHeaders);
374
- }
375
-
376
- const result = await acceptQuote(env, taskId, body.clientAddress);
377
- if ('error' in result) {
378
- return json(result, 400, corsHeaders);
379
- }
380
- return json({ task: result }, 200, corsHeaders);
381
- }
382
-
383
- // POST /api/tasks/:id/submit — Agent submits work (AUTHENTICATED: agent owner)
384
- const submitMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/submit$/);
385
- if (request.method === 'POST' && submitMatch) {
386
- const body = await request.json() as {
387
- result: string;
388
- files?: TaskFile[];
389
- signature: string;
390
- timestamp: number;
391
- nonce: string;
392
- };
393
- if (!body.result) {
394
- return json({ error: 'Missing result' }, 400, corsHeaders);
395
- }
396
- if (!body.signature || !body.timestamp) {
397
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
398
- }
399
-
400
- const taskId = submitMatch[1];
401
- const task = await getTask(env, taskId);
402
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
403
-
404
- try {
405
- const signer = await verifySigner(env, 'submit', taskId, body.timestamp, body.signature, body.nonce);
406
- const owner = await getAgentOwner(env, task.agentId);
407
- if (signer !== owner) {
408
- return json({ error: 'Unauthorized: signer is not the agent owner' }, 403, corsHeaders);
409
- }
410
- } catch (err) {
411
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
412
- }
413
-
414
- const result = await submitTask(env, taskId, body.result, body.files);
415
- if ('error' in result) {
416
- return json(result, 400, corsHeaders);
417
- }
418
- return json({ task: result }, 200, corsHeaders);
419
- }
420
-
421
- // POST /api/tasks/:id/complete — Mark completed after payment (AUTHENTICATED: task client)
422
- const completeMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/complete$/);
423
- if (request.method === 'POST' && completeMatch) {
424
- const body = await request.json() as {
425
- txHash: string;
426
- signature: string;
427
- timestamp: number;
428
- nonce: string;
429
- };
430
- if (!body.txHash) {
431
- return json({ error: 'Missing txHash' }, 400, corsHeaders);
432
- }
433
- if (!body.signature || !body.timestamp) {
434
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
435
- }
436
-
437
- const taskId = completeMatch[1];
438
- const task = await getTask(env, taskId);
439
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
440
-
441
- try {
442
- const signer = await verifySigner(env, 'complete', taskId, body.timestamp, body.signature, body.nonce);
443
- if (signer !== task.clientAddress.toLowerCase()) {
444
- return json({ error: 'Unauthorized: signer is not the task client' }, 403, corsHeaders);
445
- }
446
- } catch (err) {
447
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
448
- }
449
-
450
- const result = await completeTask(env, taskId, body.txHash);
451
- if ('error' in result) {
452
- return json(result, 400, corsHeaders);
453
- }
454
- return json({ task: result }, 200, corsHeaders);
455
- }
456
-
457
- // POST /api/tasks/:id/refund — Mark task as refunded (AUTHENTICATED: task client)
458
- const refundMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/refund$/);
459
- if (request.method === 'POST' && refundMatch) {
460
- const body = await request.json() as {
461
- txHash: string;
462
- signature: string;
463
- timestamp: number;
464
- nonce: string;
465
- };
466
- if (!body.txHash) {
467
- return json({ error: 'Missing txHash' }, 400, corsHeaders);
468
- }
469
- if (!body.signature || !body.timestamp) {
470
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
471
- }
472
-
473
- const taskId = refundMatch[1];
474
- const task = await getTask(env, taskId);
475
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
476
-
477
- try {
478
- const signer = await verifySigner(env, 'refund', taskId, body.timestamp, body.signature, body.nonce);
479
- if (signer !== task.clientAddress.toLowerCase()) {
480
- return json({ error: 'Unauthorized: signer is not the task client' }, 403, corsHeaders);
481
- }
482
- } catch (err) {
483
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
484
- }
485
-
486
- const result = await refundTask(env, taskId, body.txHash);
487
- if ('error' in result) {
488
- return json(result, 400, corsHeaders);
489
- }
490
- return json({ task: result }, 200, corsHeaders);
491
- }
492
-
493
- // POST /api/tasks/:id/rate — Mark task as rated (AUTHENTICATED: task client)
494
- const rateMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/rate$/);
495
- if (request.method === 'POST' && rateMatch) {
496
- const body = await request.json() as {
497
- txHash: string;
498
- score?: number;
499
- comment?: string;
500
- signature: string;
501
- timestamp: number;
502
- nonce: string;
503
- };
504
- if (!body.txHash) {
505
- return json({ error: 'Missing txHash' }, 400, corsHeaders);
506
- }
507
- if (!body.signature || !body.timestamp) {
508
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
509
- }
510
-
511
- const taskId = rateMatch[1];
512
- const task = await getTask(env, taskId);
513
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
514
-
515
- try {
516
- const signer = await verifySigner(env, 'rate', taskId, body.timestamp, body.signature, body.nonce);
517
- if (signer !== task.clientAddress.toLowerCase()) {
518
- return json({ error: 'Unauthorized: only the task client can rate' }, 403, corsHeaders);
519
- }
520
- } catch (err) {
521
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
522
- }
523
-
524
- const result = await rateTask(env, taskId, body.txHash, body.score, body.comment);
525
- if ('error' in result) {
526
- return json(result, 400, corsHeaders);
527
- }
528
- return json({ task: result }, 200, corsHeaders);
529
- }
530
-
531
- // POST /api/tasks/:id/revise — Client requests revision (AUTHENTICATED: task client)
532
- const reviseMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/revise$/);
533
- if (request.method === 'POST' && reviseMatch) {
534
- const body = await request.json() as {
535
- reason: string;
536
- signature: string;
537
- timestamp: number;
538
- nonce: string;
539
- };
540
- if (!body.reason) {
541
- return json({ error: 'Missing reason' }, 400, corsHeaders);
542
- }
543
- if (!body.signature || !body.timestamp) {
544
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
545
- }
546
-
547
- const taskId = reviseMatch[1];
548
- const task = await getTask(env, taskId);
549
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
550
-
551
- try {
552
- const signer = await verifySigner(env, 'revise', taskId, body.timestamp, body.signature, body.nonce);
553
- if (signer !== task.clientAddress.toLowerCase()) {
554
- return json({ error: 'Unauthorized: only the task client can request revision' }, 403, corsHeaders);
555
- }
556
- } catch (err) {
557
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
558
- }
559
-
560
- const result = await requestRevision(env, taskId, body.reason, task.clientAddress);
561
- if ('error' in result) {
562
- return json(result, 400, corsHeaders);
563
- }
564
- return json({ task: result }, 200, corsHeaders);
565
- }
566
-
567
- // POST /api/tasks/:id/message — Send a message on a task (AUTHENTICATED: client or agent owner)
568
- const messageMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/message$/);
569
- if (request.method === 'POST' && messageMatch) {
570
- const body = await request.json() as {
571
- content: string;
572
- signature: string;
573
- timestamp: number;
574
- nonce: string;
575
- };
576
- if (!body.content) {
577
- return json({ error: 'Missing content' }, 400, corsHeaders);
578
- }
579
- if (!body.signature || !body.timestamp) {
580
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
581
- }
582
-
583
- const taskId = messageMatch[1];
584
- const task = await getTask(env, taskId);
585
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
586
-
587
- // Verify signer is either client or agent owner
588
- let role: 'client' | 'agent';
589
- let senderAddress: string;
590
- try {
591
- const signer = await verifySigner(env, 'message', taskId, body.timestamp, body.signature, body.nonce);
592
- if (signer === task.clientAddress.toLowerCase()) {
593
- role = 'client';
594
- senderAddress = signer;
595
- } else {
596
- const owner = await getAgentOwner(env, task.agentId);
597
- if (signer === owner) {
598
- role = 'agent';
599
- senderAddress = signer;
600
- } else {
601
- return json({ error: 'Unauthorized: signer is not the client or agent owner' }, 403, corsHeaders);
602
- }
603
- }
604
- } catch (err) {
605
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
606
- }
607
-
608
- const result = await addMessage(env, taskId, senderAddress, role, body.content);
609
- if ('error' in result) {
610
- return json(result, 400, corsHeaders);
611
- }
612
- return json({ task: result }, 200, corsHeaders);
613
- }
614
-
615
- // POST /api/tasks/:id/upload — Upload file for a task (AUTHENTICATED: agent owner)
616
- const uploadMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/upload$/);
617
- if (request.method === 'POST' && uploadMatch) {
618
- const taskId = uploadMatch[1];
619
- const task = await getTask(env, taskId);
620
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
621
- if (task.status !== 'accepted' && task.status !== 'revision') {
622
- return json({ error: `Task is ${task.status}, can only upload when accepted or in revision` }, 400, corsHeaders);
623
- }
624
-
625
- // Auth: verify agent owner via query params (since body is file data)
626
- const signature = url.searchParams.get('signature');
627
- const timestamp = url.searchParams.get('timestamp');
628
- const nonce = url.searchParams.get('nonce');
629
- const filename = url.searchParams.get('filename') || 'file';
630
-
631
- if (!signature || !timestamp) {
632
- return json({ error: 'Missing signature or timestamp query params' }, 401, corsHeaders);
633
- }
634
-
635
- try {
636
- const signer = await verifySigner(env, 'upload', taskId, parseInt(timestamp, 10), signature, nonce || '');
637
- const owner = await getAgentOwner(env, task.agentId);
638
- if (signer !== owner) {
639
- return json({ error: 'Unauthorized: signer is not the agent owner' }, 403, corsHeaders);
640
- }
641
- } catch (err) {
642
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
643
- }
644
-
645
- const contentType = request.headers.get('Content-Type') || 'application/octet-stream';
646
- const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10);
647
-
648
- if (contentLength > 25 * 1024 * 1024) {
649
- return json({ error: 'File too large (max 25MB)' }, 413, corsHeaders);
650
- }
651
-
652
- if (!request.body) {
653
- return json({ error: 'Missing file body' }, 400, corsHeaders);
654
- }
655
-
656
- try {
657
- const file = await uploadFile(env, taskId, filename, request.body, contentType, contentLength);
658
- await addFileToTask(env, taskId, file);
659
- return json({ file }, 200, corsHeaders);
660
- } catch (err) {
661
- return json({ error: err instanceof Error ? err.message : 'Upload failed' }, 500, corsHeaders);
662
- }
663
- }
664
-
665
- // POST /api/tasks/:id/client-upload — Upload file for a task (AUTHENTICATED: task client)
666
- const clientUploadMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/client-upload$/);
667
- if (request.method === 'POST' && clientUploadMatch) {
668
- const taskId = clientUploadMatch[1];
669
- const task = await getTask(env, taskId);
670
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
671
-
672
- const terminalStatuses = ['completed', 'declined', 'expired'];
673
- if (terminalStatuses.includes(task.status)) {
674
- return json({ error: `Task is ${task.status}, cannot upload files` }, 400, corsHeaders);
675
- }
676
-
677
- const signature = url.searchParams.get('signature');
678
- const timestamp = url.searchParams.get('timestamp');
679
- const nonce = url.searchParams.get('nonce');
680
- const filename = url.searchParams.get('filename') || 'file';
681
-
682
- if (!signature || !timestamp) {
683
- return json({ error: 'Missing signature or timestamp query params' }, 401, corsHeaders);
684
- }
685
-
686
- try {
687
- const signer = await verifySigner(env, 'client-upload', taskId, parseInt(timestamp, 10), signature, nonce || '');
688
- if (signer !== task.clientAddress.toLowerCase()) {
689
- return json({ error: 'Unauthorized: signer is not the task client' }, 403, corsHeaders);
690
- }
691
- } catch (err) {
692
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
693
- }
694
-
695
- const contentType = request.headers.get('Content-Type') || 'application/octet-stream';
696
- const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10);
697
-
698
- if (contentLength > 25 * 1024 * 1024) {
699
- return json({ error: 'File too large (max 25MB)' }, 413, corsHeaders);
700
- }
701
-
702
- if (!request.body) {
703
- return json({ error: 'Missing file body' }, 400, corsHeaders);
704
- }
705
-
706
- try {
707
- const file = await uploadFile(env, taskId, filename, request.body, contentType, contentLength);
708
- await addFileToTask(env, taskId, file);
709
- return json({ file }, 200, corsHeaders);
710
- } catch (err) {
711
- return json({ error: err instanceof Error ? err.message : 'Upload failed' }, 500, corsHeaders);
712
- }
713
- }
714
-
715
- // GET /api/tasks/:id/files/:key — Download a task file
716
- const fileMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/files\/(.+)$/);
717
- if (request.method === 'GET' && fileMatch) {
718
- const taskId = fileMatch[1];
719
- const fileKey = decodeURIComponent(fileMatch[2]);
720
- const task = await getTask(env, taskId);
721
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
722
-
723
- // Only block download for declined/expired tasks
724
- if (task.status === 'declined' || task.status === 'expired') {
725
- return json({ error: 'Files not available for this task' }, 403, corsHeaders);
726
- }
727
-
728
- // Reconstruct full R2 key
729
- const fullKey = `tasks/${taskId}/${fileKey}`;
730
- const object = await downloadFile(env, fullKey);
731
- if (!object) {
732
- return json({ error: 'File not found' }, 404, corsHeaders);
733
- }
734
-
735
- return new Response(object.body, {
736
- headers: {
737
- 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
738
- 'Content-Length': String(object.size),
739
- 'Content-Disposition': `attachment; filename="${fileKey.split('/').pop() || 'file'}"`,
740
- ...corsHeaders,
741
- },
742
- });
743
- }
744
-
745
- // GET /api/agents/:id/profile — Fetch agent profile (public)
746
- const profileGetMatch = path.match(/^\/api\/agents\/([a-zA-Z0-9x]+)\/profile$/);
747
- if (request.method === 'GET' && profileGetMatch) {
748
- const profile = await getProfile(env, profileGetMatch[1]);
749
- return json({ profile: profile || { agentId: profileGetMatch[1], updatedAt: 0 } }, 200, corsHeaders);
750
- }
751
-
752
- // PUT /api/agents/:id/profile — Update agent profile (AUTHENTICATED: agent owner)
753
- if (request.method === 'PUT' && profileGetMatch) {
754
- const agentId = profileGetMatch[1];
755
- const body = await request.json() as {
756
- tagline?: string;
757
- longDescription?: string;
758
- languages?: string[];
759
- responseTime?: string;
760
- website?: string;
761
- twitter?: string;
762
- github?: string;
763
- signature: string;
764
- timestamp: number;
765
- nonce: string;
766
- };
767
-
768
- if (!body.signature || !body.timestamp) {
769
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
770
- }
771
-
772
- try {
773
- const signer = await verifySigner(env, 'profile', agentId, body.timestamp, body.signature, body.nonce);
774
- const owner = await getAgentOwner(env, agentId);
775
- if (signer !== owner) {
776
- return json({ error: 'Unauthorized: signer is not the agent owner' }, 403, corsHeaders);
777
- }
778
- } catch (err) {
779
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
780
- }
781
-
782
- const { signature: _s, timestamp: _t, nonce: _n, ...updates } = body;
783
- const profile = await updateProfile(env, agentId, updates);
784
- return json({ profile }, 200, corsHeaders);
785
- }
786
-
787
- // GET /api/agents/:id/gigs — List agent's gigs (public)
788
- const gigsGetMatch = path.match(/^\/api\/agents\/([a-zA-Z0-9x]+)\/gigs$/);
789
- if (request.method === 'GET' && gigsGetMatch) {
790
- const gigs = await getGigs(env, gigsGetMatch[1]);
791
- return json({ gigs }, 200, corsHeaders);
792
- }
793
-
794
- // POST /api/agents/:id/gigs — Create/update/delete gigs (AUTHENTICATED: agent owner)
795
- if (request.method === 'POST' && gigsGetMatch) {
796
- const agentId = gigsGetMatch[1];
797
- const body = await request.json() as {
798
- action: 'create' | 'update' | 'remove';
799
- // For create
800
- title?: string;
801
- description?: string;
802
- priceWei?: string;
803
- deliveryTime?: string;
804
- category?: string;
805
- // For remove
806
- gigId?: string;
807
- // Auth
808
- signature: string;
809
- timestamp: number;
810
- nonce: string;
811
- };
812
-
813
- if (!body.signature || !body.timestamp) {
814
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
815
- }
816
-
817
- try {
818
- const signer = await verifySigner(env, 'gig', agentId, body.timestamp, body.signature, body.nonce);
819
- const owner = await getAgentOwner(env, agentId);
820
- if (signer !== owner) {
821
- return json({ error: 'Unauthorized: signer is not the agent owner' }, 403, corsHeaders);
822
- }
823
- } catch (err) {
824
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
825
- }
826
-
827
- if (body.action === 'create') {
828
- if (!body.title || !body.description || !body.priceWei || !body.deliveryTime || !body.category) {
829
- return json({ error: 'Missing required gig fields: title, description, priceWei, deliveryTime, category' }, 400, corsHeaders);
830
- }
831
- const gig = await createGig(env, agentId, {
832
- title: body.title,
833
- description: body.description,
834
- priceWei: body.priceWei,
835
- deliveryTime: body.deliveryTime,
836
- category: body.category,
837
- });
838
- return json({ gig }, 201, corsHeaders);
839
- }
840
-
841
- if (body.action === 'update') {
842
- if (!body.gigId) {
843
- return json({ error: 'Missing gigId' }, 400, corsHeaders);
844
- }
845
- const updates: Record<string, string | undefined> = {};
846
- if (body.title) updates.title = body.title;
847
- if (body.description) updates.description = body.description;
848
- if (body.priceWei) updates.priceWei = body.priceWei;
849
- if (body.deliveryTime) updates.deliveryTime = body.deliveryTime;
850
- if (body.category) updates.category = body.category;
851
- if (Object.keys(updates).length === 0) {
852
- return json({ error: 'No fields to update' }, 400, corsHeaders);
853
- }
854
- const gig = await updateGig(env, agentId, body.gigId, updates);
855
- if (!gig) {
856
- return json({ error: 'Gig not found' }, 404, corsHeaders);
857
- }
858
- return json({ gig }, 200, corsHeaders);
859
- }
860
-
861
- if (body.action === 'remove') {
862
- if (!body.gigId) {
863
- return json({ error: 'Missing gigId' }, 400, corsHeaders);
864
- }
865
- const removed = await removeGig(env, agentId, body.gigId);
866
- if (!removed) {
867
- return json({ error: 'Gig not found' }, 404, corsHeaders);
868
- }
869
- return json({ success: true }, 200, corsHeaders);
870
- }
871
-
872
- return json({ error: 'Invalid action. Use "create", "update", or "remove"' }, 400, corsHeaders);
873
- }
874
-
875
- // POST /api/tasks/:id/dispute — Client disputes submitted work (AUTHENTICATED: task client)
876
- const disputeMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/dispute$/);
877
- if (request.method === 'POST' && disputeMatch) {
878
- const body = await request.json() as {
879
- txHash: string;
880
- signature: string;
881
- timestamp: number;
882
- nonce: string;
883
- };
884
- if (!body.txHash) {
885
- return json({ error: 'Missing txHash' }, 400, corsHeaders);
886
- }
887
- if (!body.signature || !body.timestamp) {
888
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
889
- }
890
-
891
- const taskId = disputeMatch[1];
892
- const task = await getTask(env, taskId);
893
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
894
-
895
- try {
896
- const signer = await verifySigner(env, 'dispute', taskId, body.timestamp, body.signature, body.nonce);
897
- if (signer !== task.clientAddress.toLowerCase()) {
898
- return json({ error: 'Unauthorized: signer is not the task client' }, 403, corsHeaders);
899
- }
900
- } catch (err) {
901
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
902
- }
903
-
904
- const result = await disputeTask(env, taskId, body.txHash);
905
- if ('error' in result) {
906
- return json(result, 400, corsHeaders);
907
- }
908
- return json({ task: result }, 200, corsHeaders);
909
- }
910
-
911
- // POST /api/tasks/:id/resolve — Admin resolves dispute (AUTHENTICATED: admin)
912
- const resolveMatch = path.match(/^\/api\/tasks\/([a-z0-9-]+)\/resolve$/);
913
- if (request.method === 'POST' && resolveMatch) {
914
- const body = await request.json() as {
915
- resolution: 'client' | 'agent';
916
- txHash: string;
917
- signature: string;
918
- timestamp: number;
919
- nonce: string;
920
- };
921
- if (!body.resolution || !body.txHash) {
922
- return json({ error: 'Missing resolution or txHash' }, 400, corsHeaders);
923
- }
924
- if (!body.signature || !body.timestamp) {
925
- return json({ error: 'Missing signature or timestamp' }, 401, corsHeaders);
926
- }
927
-
928
- const taskId = resolveMatch[1];
929
- const task = await getTask(env, taskId);
930
- if (!task) return json({ error: 'Task not found' }, 404, corsHeaders);
931
-
932
- // Verify signer is admin
933
- const ADMIN_ADDRESS = '0x49e54d96a6ca6f55dcc85523e47bba563dfff6f6';
934
- try {
935
- const signer = await verifySigner(env, 'resolve', taskId, body.timestamp, body.signature, body.nonce);
936
- if (signer !== ADMIN_ADDRESS) {
937
- return json({ error: 'Unauthorized: signer is not admin' }, 403, corsHeaders);
938
- }
939
- } catch (err) {
940
- return json({ error: err instanceof Error ? err.message : 'Auth failed' }, 401, corsHeaders);
941
- }
942
-
943
- const result = await resolveTask(env, taskId, body.resolution, body.txHash);
944
- if ('error' in result) {
945
- return json(result, 400, corsHeaders);
946
- }
947
- return json({ task: result }, 200, corsHeaders);
948
- }
949
-
950
- return json({ error: 'Not found' }, 404, corsHeaders);
951
- } catch (err) {
952
- console.error('Worker error:', err);
953
- return json({ error: 'Internal server error' }, 500, corsHeaders);
954
- }
955
- },
956
- };
957
-
958
- function json(data: unknown, status: number, headers: Record<string, string> = {}): Response {
959
- return new Response(JSON.stringify(data), {
960
- status,
961
- headers: { 'Content-Type': 'application/json', ...headers },
962
- });
963
- }