agentspay 0.1.0 → 0.2.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.
Files changed (126) hide show
  1. package/.env.example +40 -0
  2. package/README.md +35 -4
  3. package/VERIFICATION_SUMMARY.txt +151 -0
  4. package/dist/api/server.d.ts.map +1 -1
  5. package/dist/api/server.js +530 -74
  6. package/dist/api/server.js.map +1 -1
  7. package/dist/bsv/crypto.d.ts +65 -0
  8. package/dist/bsv/crypto.d.ts.map +1 -0
  9. package/dist/bsv/crypto.js +158 -0
  10. package/dist/bsv/crypto.js.map +1 -0
  11. package/dist/bsv/mnee.d.ts +88 -0
  12. package/dist/bsv/mnee.d.ts.map +1 -0
  13. package/dist/bsv/mnee.js +173 -0
  14. package/dist/bsv/mnee.js.map +1 -0
  15. package/dist/bsv/opreturn.d.ts +22 -0
  16. package/dist/bsv/opreturn.d.ts.map +1 -0
  17. package/dist/bsv/opreturn.js +117 -0
  18. package/dist/bsv/opreturn.js.map +1 -0
  19. package/dist/bsv/whatsonchain.d.ts +46 -0
  20. package/dist/bsv/whatsonchain.d.ts.map +1 -0
  21. package/dist/bsv/whatsonchain.js +98 -0
  22. package/dist/bsv/whatsonchain.js.map +1 -0
  23. package/dist/config.d.ts +38 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +85 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/currency/currency.d.ts +70 -0
  28. package/dist/currency/currency.d.ts.map +1 -0
  29. package/dist/currency/currency.js +137 -0
  30. package/dist/currency/currency.js.map +1 -0
  31. package/dist/disputes/dispute.d.ts +50 -0
  32. package/dist/disputes/dispute.d.ts.map +1 -0
  33. package/dist/disputes/dispute.js +162 -0
  34. package/dist/disputes/dispute.js.map +1 -0
  35. package/dist/docs/openapi.yaml +1079 -0
  36. package/dist/docs/swagger.d.ts +12 -0
  37. package/dist/docs/swagger.d.ts.map +1 -0
  38. package/dist/docs/swagger.js +104 -0
  39. package/dist/docs/swagger.js.map +1 -0
  40. package/dist/middleware/auth.d.ts +20 -0
  41. package/dist/middleware/auth.d.ts.map +1 -0
  42. package/dist/middleware/auth.js +32 -0
  43. package/dist/middleware/auth.js.map +1 -0
  44. package/dist/middleware/rateLimit.d.ts +25 -0
  45. package/dist/middleware/rateLimit.d.ts.map +1 -0
  46. package/dist/middleware/rateLimit.js +61 -0
  47. package/dist/middleware/rateLimit.js.map +1 -0
  48. package/dist/payment/payment.d.ts +79 -9
  49. package/dist/payment/payment.d.ts.map +1 -1
  50. package/dist/payment/payment.js +387 -47
  51. package/dist/payment/payment.js.map +1 -1
  52. package/dist/registry/db.d.ts.map +1 -1
  53. package/dist/registry/db.js +110 -3
  54. package/dist/registry/db.js.map +1 -1
  55. package/dist/registry/registry.d.ts +1 -1
  56. package/dist/registry/registry.d.ts.map +1 -1
  57. package/dist/registry/registry.js +12 -4
  58. package/dist/registry/registry.js.map +1 -1
  59. package/dist/types/index.d.ts +34 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/types/index.js.map +1 -1
  62. package/dist/utils/validation.d.ts +27 -0
  63. package/dist/utils/validation.d.ts.map +1 -0
  64. package/dist/utils/validation.js +164 -0
  65. package/dist/utils/validation.js.map +1 -0
  66. package/dist/verification/receipt.d.ts +42 -0
  67. package/dist/verification/receipt.d.ts.map +1 -0
  68. package/dist/verification/receipt.js +10 -0
  69. package/dist/verification/receipt.js.map +1 -0
  70. package/dist/verification/verification.d.ts +41 -0
  71. package/dist/verification/verification.d.ts.map +1 -0
  72. package/dist/verification/verification.js +217 -0
  73. package/dist/verification/verification.js.map +1 -0
  74. package/dist/wallet/providerManager.d.ts +32 -0
  75. package/dist/wallet/providerManager.d.ts.map +1 -0
  76. package/dist/wallet/providerManager.js +118 -0
  77. package/dist/wallet/providerManager.js.map +1 -0
  78. package/dist/wallet/providers/handcash.d.ts +22 -0
  79. package/dist/wallet/providers/handcash.d.ts.map +1 -0
  80. package/dist/wallet/providers/handcash.js +214 -0
  81. package/dist/wallet/providers/handcash.js.map +1 -0
  82. package/dist/wallet/providers/internal.d.ts +15 -0
  83. package/dist/wallet/providers/internal.d.ts.map +1 -0
  84. package/dist/wallet/providers/internal.js +208 -0
  85. package/dist/wallet/providers/internal.js.map +1 -0
  86. package/dist/wallet/providers/types.d.ts +50 -0
  87. package/dist/wallet/providers/types.d.ts.map +1 -0
  88. package/dist/wallet/providers/types.js +6 -0
  89. package/dist/wallet/providers/types.js.map +1 -0
  90. package/dist/wallet/providers/yours.d.ts +18 -0
  91. package/dist/wallet/providers/yours.d.ts.map +1 -0
  92. package/dist/wallet/providers/yours.js +122 -0
  93. package/dist/wallet/providers/yours.js.map +1 -0
  94. package/dist/wallet/wallet.d.ts +52 -5
  95. package/dist/wallet/wallet.d.ts.map +1 -1
  96. package/dist/wallet/wallet.js +223 -34
  97. package/dist/wallet/wallet.js.map +1 -1
  98. package/dist/webhooks/delivery.d.ts +37 -0
  99. package/dist/webhooks/delivery.d.ts.map +1 -0
  100. package/dist/webhooks/delivery.js +182 -0
  101. package/dist/webhooks/delivery.js.map +1 -0
  102. package/dist/webhooks/webhook.d.ts +85 -0
  103. package/dist/webhooks/webhook.d.ts.map +1 -0
  104. package/dist/webhooks/webhook.js +271 -0
  105. package/dist/webhooks/webhook.js.map +1 -0
  106. package/package.json +74 -54
  107. package/sdk-python/LICENSE +21 -0
  108. package/sdk-python/MANIFEST.in +9 -0
  109. package/sdk-python/README.md +372 -0
  110. package/sdk-python/agentspay/__init__.py +97 -0
  111. package/sdk-python/agentspay/client.py +256 -0
  112. package/sdk-python/agentspay/disputes.py +174 -0
  113. package/sdk-python/agentspay/exceptions.py +53 -0
  114. package/sdk-python/agentspay/payments.py +169 -0
  115. package/sdk-python/agentspay/services.py +198 -0
  116. package/sdk-python/agentspay/types.py +154 -0
  117. package/sdk-python/agentspay/wallet.py +113 -0
  118. package/sdk-python/agentspay/webhooks.py +195 -0
  119. package/sdk-python/examples/consumer.py +147 -0
  120. package/sdk-python/examples/provider.py +116 -0
  121. package/sdk-python/pyproject.toml +61 -0
  122. package/sdk-python/setup.py +53 -0
  123. package/sdk-python/tests/test_client.py +221 -0
  124. package/test-addr.js +29 -0
  125. package/test-mnee-simple.js +51 -0
  126. package/test-mnee.js +47 -0
@@ -39,31 +39,228 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.app = void 0;
40
40
  exports.startServer = startServer;
41
41
  const express_1 = __importDefault(require("express"));
42
+ const cors_1 = __importDefault(require("cors"));
43
+ const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
44
+ const net_1 = __importDefault(require("net"));
42
45
  const wallet_1 = require("../wallet/wallet");
43
46
  const registry_1 = require("../registry/registry");
44
47
  const payment_1 = require("../payment/payment");
48
+ const db_1 = require("../registry/db");
49
+ const webhook_1 = require("../webhooks/webhook");
50
+ const delivery_1 = require("../webhooks/delivery");
51
+ const dispute_1 = require("../disputes/dispute");
52
+ const swagger_1 = require("../docs/swagger");
53
+ const mnee_1 = require("../bsv/mnee");
54
+ const currency_1 = require("../currency/currency");
55
+ const verification_1 = require("../verification/verification");
45
56
  const app = (0, express_1.default)();
46
57
  exports.app = app;
58
+ app.disable('x-powered-by');
59
+ app.use((0, cors_1.default)());
47
60
  app.use(express_1.default.json());
61
+ // Setup Swagger UI documentation at /docs
62
+ (0, swagger_1.setupSwagger)(app);
63
+ // Basic API rate limiting (in-memory)
64
+ app.use('/api', (0, express_rate_limit_1.default)({
65
+ windowMs: 60_000,
66
+ limit: 100,
67
+ standardHeaders: 'draft-7',
68
+ legacyHeaders: false,
69
+ }));
48
70
  const wallets = new wallet_1.WalletManager();
49
71
  const registry = new registry_1.Registry();
50
72
  const payments = new payment_1.PaymentEngine();
73
+ const webhooks = new webhook_1.WebhookManager();
74
+ const disputes = new dispute_1.DisputeManager();
75
+ const verification = new verification_1.VerificationManager();
76
+ function getApiKey(req) {
77
+ const key = req.header('x-api-key') || req.header('authorization');
78
+ if (!key)
79
+ return null;
80
+ const m = key.match(/^Bearer\s+(.+)$/i);
81
+ return m ? m[1].trim() : key.trim();
82
+ }
83
+ function requireApiKey(req, res, next) {
84
+ const apiKey = getApiKey(req);
85
+ if (!apiKey)
86
+ return res.status(401).json({ error: 'API key required' });
87
+ const wallet = wallets.getByApiKey(apiKey);
88
+ if (!wallet)
89
+ return res.status(401).json({ error: 'Invalid API key' });
90
+ req.authWallet = wallet;
91
+ req.apiKey = apiKey;
92
+ next();
93
+ }
94
+ function requireWalletMatch(req, res, next) {
95
+ const auth = req.authWallet;
96
+ if (!auth)
97
+ return res.status(500).json({ error: 'Auth context missing' });
98
+ if (req.params.id !== auth.id)
99
+ return res.status(403).json({ error: 'Forbidden' });
100
+ next();
101
+ }
102
+ function isPrivateIp(ip) {
103
+ // IPv4 only (sufficient for audit scenarios)
104
+ const parts = ip.split('.').map(p => Number(p));
105
+ if (parts.length !== 4 || parts.some(n => !Number.isFinite(n)))
106
+ return false;
107
+ const [a, b] = parts;
108
+ if (a === 10)
109
+ return true;
110
+ if (a === 127)
111
+ return true;
112
+ if (a === 0)
113
+ return true;
114
+ if (a === 169 && b === 254)
115
+ return true;
116
+ if (a === 172 && b >= 16 && b <= 31)
117
+ return true;
118
+ if (a === 192 && b === 168)
119
+ return true;
120
+ return false;
121
+ }
122
+ function validateServiceEndpoint(endpoint) {
123
+ let u;
124
+ try {
125
+ u = new URL(endpoint);
126
+ }
127
+ catch {
128
+ throw new Error('Invalid endpoint URL');
129
+ }
130
+ if (!['http:', 'https:'].includes(u.protocol))
131
+ throw new Error('Endpoint must be http(s)');
132
+ const host = u.hostname.toLowerCase();
133
+ if (host === 'localhost' || host === '0.0.0.0')
134
+ throw new Error('Endpoint host not allowed');
135
+ if (host === '169.254.169.254')
136
+ throw new Error('Endpoint host not allowed');
137
+ if (net_1.default.isIP(host)) {
138
+ if (isPrivateIp(host))
139
+ throw new Error('Endpoint IP not allowed');
140
+ }
141
+ const port = u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80;
142
+ if (![80, 443].includes(port))
143
+ throw new Error('Endpoint port not allowed');
144
+ }
51
145
  // ============ WALLETS ============
52
- app.post('/api/wallets', (req, res) => {
146
+ // Create wallet + API key for demo/internal usage
147
+ app.post('/api/wallets/connect/internal', (_req, res) => {
53
148
  const wallet = wallets.create();
54
- res.json({ ok: true, wallet });
149
+ res.json({
150
+ ok: true,
151
+ wallet: { id: wallet.id, publicKey: wallet.publicKey, address: wallet.address, createdAt: wallet.createdAt },
152
+ apiKey: wallet.apiKey,
153
+ privateKey: wallet.privateKey,
154
+ });
55
155
  });
56
- app.get('/api/wallets/:id', async (req, res) => {
57
- const wallet = wallets.getById(req.params.id);
156
+ app.post('/api/wallets', (_req, res) => {
157
+ const wallet = wallets.create();
158
+ res.json({
159
+ ok: true,
160
+ wallet: { id: wallet.id, publicKey: wallet.publicKey, address: wallet.address, createdAt: wallet.createdAt },
161
+ apiKey: wallet.apiKey,
162
+ privateKey: wallet.privateKey,
163
+ });
164
+ });
165
+ app.post('/api/wallets/import', (req, res) => {
166
+ const { wif } = req.body;
167
+ if (!wif)
168
+ return res.status(400).json({ error: 'WIF private key required' });
169
+ try {
170
+ const wallet = wallets.importFromWif(wif);
171
+ res.json({ ok: true, wallet });
172
+ }
173
+ catch (e) {
174
+ res.status(400).json({ error: e.message });
175
+ }
176
+ });
177
+ app.get('/api/wallets/:id', requireApiKey, requireWalletMatch, async (req, res) => {
178
+ const wallet = wallets.getById(String(req.params.id));
58
179
  if (!wallet)
59
180
  return res.status(404).json({ error: 'Wallet not found' });
60
- const balance = await wallets.getBalance(req.params.id);
61
- res.json({ ok: true, wallet: { ...wallet, balance } });
181
+ const balance = await wallets.getBalance(String(req.params.id));
182
+ const balanceMnee = await mnee_1.mneeTokens.getBalance(wallet.address);
183
+ res.json({
184
+ ok: true,
185
+ wallet: {
186
+ ...wallet,
187
+ balance,
188
+ balanceBsv: balance,
189
+ balanceMnee,
190
+ balances: {
191
+ BSV: { amount: balance, formatted: currency_1.CurrencyManager.format(balance, 'BSV') },
192
+ MNEE: { amount: balanceMnee, formatted: currency_1.CurrencyManager.format(balanceMnee, 'MNEE') }
193
+ }
194
+ }
195
+ });
196
+ });
197
+ // ============ CURRENCY ============
198
+ app.get('/api/rates', async (_req, res) => {
199
+ try {
200
+ const bsvToMnee = await currency_1.CurrencyManager.getConversionRate('BSV', 'MNEE');
201
+ const mneeToBsv = await currency_1.CurrencyManager.getConversionRate('MNEE', 'BSV');
202
+ res.json({
203
+ ok: true,
204
+ rates: {
205
+ BSV_to_MNEE: bsvToMnee,
206
+ MNEE_to_BSV: mneeToBsv
207
+ },
208
+ currencies: {
209
+ BSV: currency_1.CurrencyManager.getConfig('BSV'),
210
+ MNEE: currency_1.CurrencyManager.getConfig('MNEE')
211
+ }
212
+ });
213
+ }
214
+ catch (e) {
215
+ res.status(500).json({ error: e.message });
216
+ }
217
+ });
218
+ app.post('/api/wallets/:id/fund-mnee', requireApiKey, requireWalletMatch, async (req, res) => {
219
+ try {
220
+ const { amount } = req.body;
221
+ const wallet = wallets.getById(String(req.params.id));
222
+ if (!wallet)
223
+ return res.status(404).json({ error: 'Wallet not found' });
224
+ const amountCents = Number(amount);
225
+ if (!Number.isInteger(amountCents) || amountCents <= 0) {
226
+ throw new Error('Invalid amount (must be positive integer in cents)');
227
+ }
228
+ await mnee_1.mneeTokens.fundDemo(wallet.address, amountCents);
229
+ const newBalance = await mnee_1.mneeTokens.getBalance(wallet.address);
230
+ res.json({
231
+ ok: true,
232
+ message: `Funded ${amountCents} MNEE cents ($${(amountCents / 100).toFixed(2)})`,
233
+ balance: newBalance,
234
+ balanceFormatted: currency_1.CurrencyManager.format(newBalance, 'MNEE')
235
+ });
236
+ }
237
+ catch (e) {
238
+ res.status(400).json({ error: e.message });
239
+ }
62
240
  });
63
241
  // ============ SERVICES (Registry) ============
64
- app.post('/api/services', (req, res) => {
242
+ app.post('/api/services', requireApiKey, (req, res) => {
65
243
  try {
66
- const service = registry.register(req.body);
244
+ const auth = req.authWallet;
245
+ if (req.body?.agentId !== auth.id)
246
+ return res.status(403).json({ error: 'Forbidden' });
247
+ if (typeof req.body?.name !== 'string' || req.body.name.length < 1 || req.body.name.length > 120) {
248
+ throw new Error('Invalid name');
249
+ }
250
+ if (typeof req.body?.description !== 'string' || req.body.description.length < 1 || req.body.description.length > 2000) {
251
+ throw new Error('Invalid description');
252
+ }
253
+ if (/<script\b/i.test(req.body.description))
254
+ throw new Error('Invalid description');
255
+ const price = Number(req.body?.price);
256
+ if (!Number.isInteger(price) || price <= 0 || price > 100000000)
257
+ throw new Error('Invalid price');
258
+ const currency = req.body?.currency || 'BSV';
259
+ if (currency !== 'BSV' && currency !== 'MNEE') {
260
+ throw new Error('Invalid currency. Must be BSV or MNEE');
261
+ }
262
+ validateServiceEndpoint(String(req.body?.endpoint));
263
+ const service = registry.register({ ...req.body, currency });
67
264
  res.json({ ok: true, service });
68
265
  }
69
266
  catch (e) {
@@ -72,8 +269,8 @@ app.post('/api/services', (req, res) => {
72
269
  });
73
270
  app.get('/api/services', (req, res) => {
74
271
  const services = registry.search({
75
- category: req.query.category,
76
- keyword: req.query.q,
272
+ category: typeof req.query.category === 'string' ? req.query.category : undefined,
273
+ keyword: typeof req.query.q === 'string' ? req.query.q : undefined,
77
274
  maxPrice: req.query.maxPrice ? Number(req.query.maxPrice) : undefined,
78
275
  limit: req.query.limit ? Number(req.query.limit) : undefined,
79
276
  offset: req.query.offset ? Number(req.query.offset) : undefined,
@@ -81,15 +278,32 @@ app.get('/api/services', (req, res) => {
81
278
  res.json({ ok: true, services });
82
279
  });
83
280
  app.get('/api/services/:id', (req, res) => {
84
- const service = registry.getById(req.params.id);
281
+ const service = registry.getById(String(req.params.id));
85
282
  if (!service)
86
283
  return res.status(404).json({ error: 'Service not found' });
87
284
  res.json({ ok: true, service });
88
285
  });
89
- app.patch('/api/services/:id', (req, res) => {
90
- const service = registry.update(req.params.id, req.body);
91
- if (!service)
286
+ app.patch('/api/services/:id', requireApiKey, (req, res) => {
287
+ const auth = req.authWallet;
288
+ const existing = registry.getById(String(req.params.id));
289
+ if (!existing)
92
290
  return res.status(404).json({ error: 'Service not found' });
291
+ if (existing.agentId !== auth.id)
292
+ return res.status(403).json({ error: 'Forbidden' });
293
+ if (req.body?.price !== undefined) {
294
+ const price = Number(req.body.price);
295
+ if (!Number.isInteger(price) || price <= 0 || price > 100000000)
296
+ return res.status(400).json({ error: 'Invalid price' });
297
+ }
298
+ if (req.body?.endpoint) {
299
+ try {
300
+ validateServiceEndpoint(String(req.body.endpoint));
301
+ }
302
+ catch (e) {
303
+ return res.status(400).json({ error: e.message });
304
+ }
305
+ }
306
+ const service = registry.update(String(req.params.id), req.body);
93
307
  res.json({ ok: true, service });
94
308
  });
95
309
  // ============ EXECUTE (Pay + Run) ============
@@ -100,116 +314,358 @@ app.post('/api/execute/:serviceId', async (req, res) => {
100
314
  return res.status(404).json({ error: 'Service not found' });
101
315
  if (!service.active)
102
316
  return res.status(400).json({ error: 'Service is inactive' });
317
+ try {
318
+ validateServiceEndpoint(service.endpoint);
319
+ }
320
+ catch (e) {
321
+ return res.status(400).json({ error: `Unsafe service endpoint: ${e.message}` });
322
+ }
103
323
  const buyer = wallets.getById(buyerWalletId);
104
324
  if (!buyer)
105
325
  return res.status(404).json({ error: 'Buyer wallet not found' });
106
- // Check balance
107
- const balance = await wallets.getBalance(buyerWalletId);
326
+ const currency = service.currency || 'BSV';
327
+ // Check balance based on currency
328
+ let balance = 0;
329
+ if (currency === 'BSV') {
330
+ balance = await wallets.getBalance(buyerWalletId);
331
+ }
332
+ else if (currency === 'MNEE') {
333
+ balance = await mnee_1.mneeTokens.getBalance(buyer.address);
334
+ }
108
335
  if (balance < service.price) {
109
336
  return res.status(402).json({
110
- error: 'Insufficient funds',
337
+ error: `Insufficient ${currency} balance`,
111
338
  required: service.price,
339
+ requiredFormatted: currency_1.CurrencyManager.format(service.price, currency),
112
340
  available: balance,
113
- address: buyer.address, // Send BSV here to top up
341
+ availableFormatted: currency_1.CurrencyManager.format(balance, currency),
342
+ currency,
343
+ address: buyer.address,
114
344
  });
115
345
  }
116
- // Create escrow payment
117
- const payment = payments.create(service.id, buyerWalletId, service.agentId, service.price);
118
- // Execute the service
119
- const startTime = Date.now();
120
346
  try {
121
- const response = await fetch(service.endpoint, {
122
- method: service.method,
123
- headers: { 'Content-Type': 'application/json' },
124
- body: JSON.stringify(input),
125
- });
126
- if (!response.ok) {
127
- // Service failed refund
128
- payments.refund(payment.id);
129
- return res.status(502).json({
130
- error: 'Service execution failed',
347
+ const payment = await payments.create(service.id, buyerWalletId, service.agentId, service.price, currency);
348
+ const startTime = Date.now();
349
+ const timeoutMs = (service.timeout || 30) * 1000;
350
+ try {
351
+ // Execute with timeout
352
+ const controller = new AbortController();
353
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
354
+ const response = await fetch(service.endpoint, {
355
+ method: service.method,
356
+ headers: { 'Content-Type': 'application/json' },
357
+ body: JSON.stringify(input),
358
+ signal: controller.signal,
359
+ }).finally(() => clearTimeout(timeoutId));
360
+ if (!response.ok) {
361
+ await payments.refund(payment.id);
362
+ return res.status(502).json({
363
+ error: 'Service execution failed',
364
+ paymentId: payment.id,
365
+ status: 'refunded',
366
+ });
367
+ }
368
+ const output = await response.json();
369
+ const executionTimeMs = Date.now() - startTime;
370
+ // Mark payment as completed (starts dispute window)
371
+ const db = (0, db_1.getDb)();
372
+ db.prepare('UPDATE payments SET completedAt = ? WHERE id = ?').run(new Date().toISOString(), payment.id);
373
+ // Create execution receipt (cryptographic proof)
374
+ const receipt = await verification.createReceipt(payment, input || {}, output || {}, executionTimeMs);
375
+ // Release payment immediately (buyer has dispute window to file dispute)
376
+ await payments.release(payment.id);
377
+ // Trigger service.executed webhook (include receipt)
378
+ delivery_1.webhookDelivery.trigger('service.executed', {
379
+ serviceId: service.id,
380
+ paymentId: payment.id,
381
+ executionTimeMs,
382
+ output,
383
+ receipt,
384
+ }).catch(console.error);
385
+ // Check for expired dispute windows
386
+ disputes.checkExpiredWindows();
387
+ res.json({
388
+ ok: true,
131
389
  paymentId: payment.id,
132
- status: 'refunded',
390
+ output,
391
+ executionTimeMs,
392
+ cost: {
393
+ amount: service.price,
394
+ amountFormatted: currency_1.CurrencyManager.format(service.price, currency),
395
+ platformFee: payment.platformFee,
396
+ platformFeeFormatted: currency_1.CurrencyManager.format(payment.platformFee, currency),
397
+ currency,
398
+ },
399
+ txId: payment.txId,
400
+ disputeWindowMinutes: service.disputeWindow || 30,
401
+ receipt,
133
402
  });
134
403
  }
135
- const output = await response.json();
136
- const executionTimeMs = Date.now() - startTime;
137
- // Success → release payment
138
- payments.release(payment.id);
139
- res.json({
140
- ok: true,
141
- paymentId: payment.id,
142
- output,
143
- executionTimeMs,
144
- cost: {
145
- amount: service.price,
146
- platformFee: payment.platformFee,
147
- currency: 'satoshis',
148
- },
149
- });
404
+ catch (e) {
405
+ // Handle timeout or service failure
406
+ await payments.refund(payment.id);
407
+ if (e.name === 'AbortError') {
408
+ res.status(504).json({
409
+ error: `Service timeout after ${service.timeout || 30}s`,
410
+ paymentId: payment.id,
411
+ status: 'refunded',
412
+ });
413
+ }
414
+ else {
415
+ res.status(502).json({
416
+ error: `Service unreachable: ${e.message}`,
417
+ paymentId: payment.id,
418
+ status: 'refunded',
419
+ });
420
+ }
421
+ }
150
422
  }
151
423
  catch (e) {
152
- // Network error refund
153
- payments.refund(payment.id);
154
- res.status(502).json({
155
- error: `Service unreachable: ${e.message}`,
156
- paymentId: payment.id,
157
- status: 'refunded',
158
- });
424
+ res.status(500).json({ error: `Payment creation failed: ${e.message}` });
159
425
  }
160
426
  });
161
427
  // ============ PAYMENTS ============
162
428
  app.get('/api/payments/:id', (req, res) => {
163
- const payment = payments.getById(req.params.id);
429
+ const payment = payments.getById(String(req.params.id));
164
430
  if (!payment)
165
431
  return res.status(404).json({ error: 'Payment not found' });
166
432
  res.json({ ok: true, payment });
167
433
  });
168
434
  app.post('/api/payments/:id/dispute', (req, res) => {
169
- const payment = payments.dispute(req.params.id);
435
+ const payment = payments.dispute(String(req.params.id));
170
436
  if (!payment)
171
437
  return res.status(400).json({ error: 'Cannot dispute this payment' });
172
438
  res.json({ ok: true, payment });
173
439
  });
440
+ // ============ DISPUTES ============
441
+ app.post('/api/disputes', requireApiKey, (req, res) => {
442
+ try {
443
+ const auth = req.authWallet;
444
+ const { paymentId, reason, evidence } = req.body;
445
+ if (!paymentId || typeof paymentId !== 'string') {
446
+ return res.status(400).json({ error: 'paymentId required' });
447
+ }
448
+ if (!reason || typeof reason !== 'string' || reason.length < 10 || reason.length > 2000) {
449
+ return res.status(400).json({ error: 'Reason required (10-2000 chars)' });
450
+ }
451
+ if (evidence !== undefined && (typeof evidence !== 'string' || evidence.length > 10000)) {
452
+ return res.status(400).json({ error: 'Evidence too long (max 10000 chars)' });
453
+ }
454
+ const dispute = disputes.create(paymentId, auth.id, reason, evidence);
455
+ res.json({ ok: true, dispute });
456
+ }
457
+ catch (e) {
458
+ res.status(400).json({ error: e.message });
459
+ }
460
+ });
461
+ app.get('/api/disputes/:id', requireApiKey, (req, res) => {
462
+ const dispute = disputes.getById(String(req.params.id));
463
+ if (!dispute)
464
+ return res.status(404).json({ error: 'Dispute not found' });
465
+ const auth = req.authWallet;
466
+ if (dispute.buyerWalletId !== auth.id && dispute.providerWalletId !== auth.id) {
467
+ return res.status(403).json({ error: 'Forbidden' });
468
+ }
469
+ res.json({ ok: true, dispute });
470
+ });
471
+ app.get('/api/disputes', requireApiKey, (req, res) => {
472
+ const auth = req.authWallet;
473
+ const status = typeof req.query.status === 'string' ? req.query.status : undefined;
474
+ const disputesList = disputes.listByWallet(auth.id, status);
475
+ res.json({ ok: true, disputes: disputesList });
476
+ });
477
+ app.post('/api/disputes/:id/resolve', requireApiKey, (req, res) => {
478
+ try {
479
+ // TODO: Add admin check here (for now, any authenticated user can resolve)
480
+ // In production, this should be restricted to platform admins
481
+ const { resolution, splitPercent } = req.body;
482
+ if (!resolution || !['refund', 'release', 'split'].includes(resolution)) {
483
+ return res.status(400).json({ error: 'Invalid resolution. Must be: refund, release, or split' });
484
+ }
485
+ const dispute = disputes.resolve(String(req.params.id), { resolution, splitPercent });
486
+ if (!dispute)
487
+ return res.status(404).json({ error: 'Dispute not found' });
488
+ res.json({ ok: true, dispute });
489
+ }
490
+ catch (e) {
491
+ res.status(400).json({ error: e.message });
492
+ }
493
+ });
174
494
  // ============ REPUTATION ============
175
495
  app.get('/api/agents/:id/reputation', (req, res) => {
176
- const reputation = registry.getReputation(req.params.id);
496
+ const reputation = registry.getReputation(String(req.params.id));
177
497
  res.json({ ok: true, reputation });
178
498
  });
499
+ // ============ WALLET UTXOS ============
500
+ app.get('/api/wallets/:id/utxos', async (req, res) => {
501
+ const apiKey = getApiKey(req);
502
+ if (!apiKey)
503
+ return res.status(401).json({ error: 'API key required' });
504
+ if (!wallets.verifyApiKey(String(req.params.id), apiKey))
505
+ return res.status(403).json({ error: 'Forbidden' });
506
+ const wallet = wallets.getById(String(req.params.id));
507
+ if (!wallet)
508
+ return res.status(404).json({ error: 'Wallet not found' });
509
+ try {
510
+ const utxos = await wallets.getUtxos(String(req.params.id));
511
+ res.json({ ok: true, utxos });
512
+ }
513
+ catch (e) {
514
+ res.status(500).json({ error: e.message });
515
+ }
516
+ });
517
+ // ============ WALLET TX HISTORY ============
518
+ app.get('/api/wallets/:id/transactions', async (req, res) => {
519
+ const apiKey = getApiKey(req);
520
+ if (!apiKey)
521
+ return res.status(401).json({ error: 'API key required' });
522
+ if (!wallets.verifyApiKey(String(req.params.id), apiKey))
523
+ return res.status(403).json({ error: 'Forbidden' });
524
+ const wallet = wallets.getById(String(req.params.id));
525
+ if (!wallet)
526
+ return res.status(404).json({ error: 'Wallet not found' });
527
+ try {
528
+ const transactions = await wallets.getTxHistory(String(req.params.id));
529
+ res.json({ ok: true, transactions });
530
+ }
531
+ catch (e) {
532
+ res.status(500).json({ error: e.message });
533
+ }
534
+ });
179
535
  // ============ FUND (testnet/demo only) ============
180
- app.post('/api/wallets/:id/fund', async (req, res) => {
181
- const { amount } = req.body; // satoshis
182
- if (!amount || amount <= 0)
536
+ app.post('/api/wallets/:id/fund', requireApiKey, requireWalletMatch, async (req, res) => {
537
+ const { amount } = req.body;
538
+ if (!Number.isInteger(amount) || amount <= 0 || amount > 100000000)
183
539
  return res.status(400).json({ error: 'Invalid amount' });
184
- const wallet = wallets.getById(req.params.id);
540
+ const wallet = wallets.getById(String(req.params.id));
185
541
  if (!wallet)
186
542
  return res.status(404).json({ error: 'Wallet not found' });
187
- // Credit balance directly via a dedicated deposits table
188
- const { getDb: getDatabase } = await Promise.resolve().then(() => __importStar(require('../registry/db')));
189
- const { v4: uuidv4 } = await Promise.resolve().then(() => __importStar(require('uuid')));
190
- const db = getDatabase();
191
- // Ensure deposits table exists
192
- db.exec(`CREATE TABLE IF NOT EXISTS deposits (
193
- id TEXT PRIMARY KEY, walletId TEXT NOT NULL, amount INTEGER NOT NULL, createdAt TEXT NOT NULL
543
+ const db = (0, db_1.getDb)();
544
+ db.exec(`CREATE TABLE IF NOT EXISTS deposits (
545
+ id TEXT PRIMARY KEY, walletId TEXT NOT NULL, amount INTEGER NOT NULL, createdAt TEXT NOT NULL
194
546
  )`);
195
- db.prepare(`INSERT INTO deposits (id, walletId, amount, createdAt) VALUES (?, ?, ?, datetime('now'))`)
196
- .run(uuidv4(), req.params.id, amount);
197
- const balance = await wallets.getBalance(req.params.id);
198
- res.json({ ok: true, funded: amount, balance });
547
+ const { v4: uuidv4 } = await Promise.resolve().then(() => __importStar(require('uuid')));
548
+ db.prepare(`INSERT INTO deposits (id, walletId, amount, createdAt) VALUES (?, ?, ?, datetime('now'))`).run(uuidv4(), String(req.params.id), amount);
549
+ const balance = await wallets.getBalance(String(req.params.id));
550
+ res.json({ ok: true, funded: amount, balance, mode: 'internal-ledger' });
551
+ });
552
+ // ============ WEBHOOKS ============
553
+ app.post('/api/webhooks', requireApiKey, (req, res) => {
554
+ try {
555
+ const auth = req.authWallet;
556
+ const { url, events } = req.body;
557
+ if (!url || typeof url !== 'string') {
558
+ return res.status(400).json({ error: 'Valid URL required' });
559
+ }
560
+ if (!Array.isArray(events) || events.length === 0) {
561
+ return res.status(400).json({ error: 'At least one event required' });
562
+ }
563
+ const webhook = webhooks.register({ url, events, ownerId: auth.id });
564
+ res.json({ ok: true, webhook });
565
+ }
566
+ catch (e) {
567
+ res.status(400).json({ error: e.message });
568
+ }
569
+ });
570
+ app.get('/api/webhooks', requireApiKey, (req, res) => {
571
+ const auth = req.authWallet;
572
+ const webhookList = webhooks.list(auth.id);
573
+ res.json({ ok: true, webhooks: webhookList });
574
+ });
575
+ app.put('/api/webhooks/:id', requireApiKey, (req, res) => {
576
+ try {
577
+ const auth = req.authWallet;
578
+ const { url, events, active } = req.body;
579
+ const updates = {};
580
+ if (url !== undefined) {
581
+ if (typeof url !== 'string') {
582
+ return res.status(400).json({ error: 'URL must be a string' });
583
+ }
584
+ updates.url = url;
585
+ }
586
+ if (events !== undefined) {
587
+ if (!Array.isArray(events)) {
588
+ return res.status(400).json({ error: 'Events must be an array' });
589
+ }
590
+ updates.events = events;
591
+ }
592
+ if (active !== undefined)
593
+ updates.active = active;
594
+ const webhook = webhooks.update(String(req.params.id), updates, auth.id);
595
+ if (!webhook)
596
+ return res.status(404).json({ error: 'Webhook not found' });
597
+ res.json({ ok: true, webhook });
598
+ }
599
+ catch (e) {
600
+ res.status(400).json({ error: e.message });
601
+ }
602
+ });
603
+ app.delete('/api/webhooks/:id', requireApiKey, (req, res) => {
604
+ try {
605
+ const auth = req.authWallet;
606
+ const deleted = webhooks.delete(req.params.id, auth.id);
607
+ if (!deleted)
608
+ return res.status(404).json({ error: 'Webhook not found' });
609
+ res.json({ ok: true });
610
+ }
611
+ catch (e) {
612
+ res.status(400).json({ error: e.message });
613
+ }
614
+ });
615
+ // ============ RECEIPTS (Execution Verification) ============
616
+ app.get('/api/receipts/:paymentId', (req, res) => {
617
+ try {
618
+ const receipt = verification.getReceipt(String(req.params.paymentId));
619
+ if (!receipt) {
620
+ return res.status(404).json({ error: 'Receipt not found' });
621
+ }
622
+ res.json({ ok: true, receipt });
623
+ }
624
+ catch (e) {
625
+ res.status(500).json({ error: e.message });
626
+ }
627
+ });
628
+ app.get('/api/receipts/:paymentId/verify', async (req, res) => {
629
+ try {
630
+ const receipt = verification.getReceipt(String(req.params.paymentId));
631
+ if (!receipt) {
632
+ return res.status(404).json({ error: 'Receipt not found' });
633
+ }
634
+ const result = await verification.verifyReceipt(receipt);
635
+ res.json({
636
+ ok: true,
637
+ verification: result,
638
+ });
639
+ }
640
+ catch (e) {
641
+ res.status(500).json({ error: e.message });
642
+ }
199
643
  });
200
644
  // ============ HEALTH ============
201
645
  app.get('/api/health', (_req, res) => {
202
646
  res.json({ ok: true, service: 'agentpay', version: '0.1.0' });
203
647
  });
204
- // ============ START ============
648
+ // JSON parse error handler (avoid stack traces)
649
+ app.use((err, _req, res, next) => {
650
+ if (err && err.type === 'entity.parse.failed') {
651
+ return res.status(400).json({ error: 'Invalid JSON' });
652
+ }
653
+ if (err instanceof SyntaxError && 'body' in err) {
654
+ return res.status(400).json({ error: 'Invalid JSON' });
655
+ }
656
+ return next(err);
657
+ });
205
658
  const PORT = Number(process.env.PORT) || 3100;
206
659
  function startServer() {
207
660
  app.listen(PORT, () => {
208
661
  console.log(`🚀 AgentPay API running on http://localhost:${PORT}`);
662
+ console.log(`📚 API Docs: http://localhost:${PORT}/docs`);
209
663
  console.log(`📋 Registry: GET /api/services`);
210
664
  console.log(`💰 Execute: POST /api/execute/:serviceId`);
211
665
  console.log(`👛 Wallets: POST /api/wallets`);
212
666
  });
213
667
  return app;
214
668
  }
669
+ // Auto-start server
670
+ startServer();
215
671
  //# sourceMappingURL=server.js.map