@sip-protocol/api 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/routes/index.ts
@@ -23,35 +33,445 @@ __export(routes_exports, {
23
33
  default: () => routes_default
24
34
  });
25
35
  module.exports = __toCommonJS(routes_exports);
26
- var import_express6 = require("express");
36
+ var import_express7 = require("express");
27
37
 
28
38
  // src/routes/health.ts
29
- var import_express = require("express");
30
- var router = (0, import_express.Router)();
31
- var startTime = Date.now();
32
- router.get("/", (req, res) => {
33
- const response = {
34
- success: true,
35
- data: {
36
- status: "healthy",
37
- version: process.env.npm_package_version || "0.1.0",
38
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
39
- uptime: Math.floor((Date.now() - startTime) / 1e3)
39
+ var import_express2 = require("express");
40
+
41
+ // src/logger.ts
42
+ var import_pino = __toESM(require("pino"));
43
+ var import_pino_http = __toESM(require("pino-http"));
44
+ var import_crypto = __toESM(require("crypto"));
45
+
46
+ // src/config.ts
47
+ var import_envalid = require("envalid");
48
+ var env = (0, import_envalid.cleanEnv)(process.env, {
49
+ // Server configuration
50
+ NODE_ENV: (0, import_envalid.str)({
51
+ choices: ["development", "production", "test"],
52
+ default: "development",
53
+ desc: "Application environment"
54
+ }),
55
+ PORT: (0, import_envalid.port)({
56
+ default: 3e3,
57
+ desc: "Server port"
58
+ }),
59
+ // CORS configuration
60
+ CORS_ORIGINS: (0, import_envalid.str)({
61
+ default: "",
62
+ desc: "Comma-separated list of allowed origins (empty = localhost only in dev)"
63
+ }),
64
+ // Authentication
65
+ API_KEYS: (0, import_envalid.str)({
66
+ default: "",
67
+ desc: "Comma-separated list of valid API keys (empty = auth disabled in dev)"
68
+ }),
69
+ // Rate limiting
70
+ RATE_LIMIT_MAX: (0, import_envalid.num)({
71
+ default: 100,
72
+ desc: "Maximum requests per window"
73
+ }),
74
+ RATE_LIMIT_WINDOW_MS: (0, import_envalid.num)({
75
+ default: 6e4,
76
+ desc: "Rate limit window in milliseconds"
77
+ }),
78
+ // Proxy trust configuration (for X-Forwarded-For header)
79
+ // Set to number of trusted proxies (e.g., 1 for single nginx)
80
+ // or 'loopback' for local proxies, or 'uniquelocal' for private IPs
81
+ TRUST_PROXY: (0, import_envalid.str)({
82
+ default: "1",
83
+ desc: 'Express trust proxy setting (number of hops, "loopback", "uniquelocal", or "false")'
84
+ }),
85
+ // Logging
86
+ LOG_LEVEL: (0, import_envalid.str)({
87
+ choices: ["trace", "debug", "info", "warn", "error", "fatal"],
88
+ default: "info",
89
+ desc: "Logging level"
90
+ }),
91
+ // Graceful shutdown
92
+ SHUTDOWN_TIMEOUT_MS: (0, import_envalid.num)({
93
+ default: 3e4,
94
+ desc: "Graceful shutdown timeout in milliseconds"
95
+ }),
96
+ // Monitoring
97
+ SENTRY_DSN: (0, import_envalid.str)({
98
+ default: "",
99
+ desc: "Sentry DSN for error tracking (optional)"
100
+ }),
101
+ METRICS_ENABLED: (0, import_envalid.str)({
102
+ choices: ["true", "false"],
103
+ default: "true",
104
+ desc: "Enable Prometheus metrics endpoint"
105
+ }),
106
+ // Webhook configuration
107
+ HELIUS_WEBHOOK_SECRET: (0, import_envalid.str)({
108
+ default: "",
109
+ desc: "Helius webhook HMAC secret for signature verification (optional)"
110
+ }),
111
+ WEBHOOK_DELIVERY_MAX_RETRIES: (0, import_envalid.num)({
112
+ default: 3,
113
+ desc: "Maximum delivery attempts for webhook notifications"
114
+ }),
115
+ WEBHOOK_STORE_MAX_SIZE: (0, import_envalid.num)({
116
+ default: 1e3,
117
+ desc: "Maximum number of registered webhooks"
118
+ })
119
+ });
120
+ var isProduction = env.isProduction;
121
+ var isDevelopment = env.isDevelopment;
122
+ var isTest = env.isTest;
123
+
124
+ // src/logger.ts
125
+ var loggerConfig = {
126
+ level: env.LOG_LEVEL,
127
+ base: {
128
+ service: "sip-api",
129
+ version: "0.1.0"
130
+ },
131
+ timestamp: import_pino.default.stdTimeFunctions.isoTime
132
+ };
133
+ if (env.isDevelopment) {
134
+ loggerConfig.transport = {
135
+ target: "pino-pretty",
136
+ options: {
137
+ colorize: true,
138
+ translateTime: "HH:MM:ss",
139
+ ignore: "pid,hostname,service,version"
40
140
  }
41
141
  };
42
- res.json(response);
142
+ }
143
+ var logger = (0, import_pino.default)(loggerConfig);
144
+ var requestLogger = (0, import_pino_http.default)({
145
+ logger,
146
+ // Use requestId from requestIdMiddleware (set earlier in chain)
147
+ // Falls back to header or generates new if middleware didn't run
148
+ genReqId: (req) => {
149
+ const augmentedReq = req;
150
+ if (augmentedReq.requestId) {
151
+ return augmentedReq.requestId;
152
+ }
153
+ const existingId = req.headers["x-request-id"];
154
+ if (existingId && typeof existingId === "string") {
155
+ return existingId;
156
+ }
157
+ return import_crypto.default.randomUUID();
158
+ },
159
+ customLogLevel: (_req, res, err) => {
160
+ if (res.statusCode >= 500 || err) return "error";
161
+ if (res.statusCode >= 400) return "warn";
162
+ return "info";
163
+ },
164
+ customSuccessMessage: (req, res) => {
165
+ return `${req.method} ${req.url} ${res.statusCode}`;
166
+ },
167
+ customErrorMessage: (req, res, err) => {
168
+ return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
169
+ },
170
+ // Don't log health checks in production
171
+ autoLogging: {
172
+ ignore: (req) => {
173
+ return req.url === "/api/v1/health" && env.isProduction;
174
+ }
175
+ }
43
176
  });
44
- var health_default = router;
45
177
 
46
- // src/routes/stealth.ts
47
- var import_express2 = require("express");
178
+ // src/shutdown.ts
179
+ var isShuttingDown = false;
180
+ function isServerShuttingDown() {
181
+ return isShuttingDown;
182
+ }
183
+
184
+ // src/stores/swap-store.ts
185
+ var import_lru_cache = require("lru-cache");
186
+ var DEFAULT_MAX_SIZE = 1e4;
187
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
188
+ var SwapStore = class {
189
+ cache;
190
+ maxSize;
191
+ ttlMs;
192
+ constructor(config = {}) {
193
+ this.maxSize = config.maxSize ?? DEFAULT_MAX_SIZE;
194
+ this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
195
+ this.cache = new import_lru_cache.LRUCache({
196
+ max: this.maxSize,
197
+ ttl: this.ttlMs,
198
+ updateAgeOnGet: false,
199
+ // Don't reset TTL on read
200
+ updateAgeOnHas: false,
201
+ // Log when items are evicted or expired
202
+ disposeAfter: (_value, key, reason) => {
203
+ if (reason === "evict") {
204
+ logger.debug({ swapId: key, reason }, "Swap evicted from cache (LRU)");
205
+ } else if (reason === "expire") {
206
+ logger.debug({ swapId: key, reason }, "Swap expired from cache (TTL)");
207
+ }
208
+ }
209
+ });
210
+ logger.info(
211
+ { maxSize: this.maxSize, ttlMs: this.ttlMs },
212
+ "SwapStore initialized"
213
+ );
214
+ }
215
+ /**
216
+ * Get a swap by ID
217
+ */
218
+ get(id) {
219
+ return this.cache.get(id);
220
+ }
221
+ /**
222
+ * Check if a swap exists
223
+ */
224
+ has(id) {
225
+ return this.cache.has(id);
226
+ }
227
+ /**
228
+ * Set/update a swap
229
+ */
230
+ set(id, data) {
231
+ this.cache.set(id, data);
232
+ }
233
+ /**
234
+ * Delete a swap
235
+ */
236
+ delete(id) {
237
+ return this.cache.delete(id);
238
+ }
239
+ /**
240
+ * Update swap status
241
+ */
242
+ updateStatus(id, status, updates) {
243
+ const existing = this.cache.get(id);
244
+ if (!existing) {
245
+ return void 0;
246
+ }
247
+ const updated = {
248
+ ...existing,
249
+ ...updates,
250
+ status,
251
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
252
+ };
253
+ this.cache.set(id, updated);
254
+ return updated;
255
+ }
256
+ /**
257
+ * Get store metrics for monitoring
258
+ */
259
+ getMetrics() {
260
+ const size = this.cache.size;
261
+ return {
262
+ size,
263
+ maxSize: this.maxSize,
264
+ ttlMs: this.ttlMs,
265
+ utilizationPercent: Math.round(size / this.maxSize * 100)
266
+ };
267
+ }
268
+ /**
269
+ * Clear all swaps (for testing)
270
+ */
271
+ clear() {
272
+ this.cache.clear();
273
+ }
274
+ /**
275
+ * Purge stale entries (normally handled automatically by LRU cache)
276
+ */
277
+ purgeStale() {
278
+ this.cache.purgeStale();
279
+ }
280
+ };
281
+ var swapStore = new SwapStore();
282
+
283
+ // src/stores/webhook-store.ts
284
+ var import_crypto2 = require("crypto");
285
+ var WebhookStore = class {
286
+ registrations = /* @__PURE__ */ new Map();
287
+ maxSize;
288
+ constructor(maxSize) {
289
+ this.maxSize = maxSize ?? env.WEBHOOK_STORE_MAX_SIZE;
290
+ logger.info({ maxSize: this.maxSize }, "WebhookStore initialized");
291
+ }
292
+ /**
293
+ * Register a new webhook
294
+ *
295
+ * @returns The registration with secret (shown once only)
296
+ */
297
+ register(url, viewingPrivateKey, spendingPublicKey) {
298
+ if (this.registrations.size >= this.maxSize) {
299
+ throw new Error("WEBHOOK_STORE_FULL");
300
+ }
301
+ const id = (0, import_crypto2.randomBytes)(16).toString("hex");
302
+ const secret = (0, import_crypto2.randomBytes)(32).toString("hex");
303
+ const registration = {
304
+ id,
305
+ url,
306
+ viewingPrivateKey,
307
+ spendingPublicKey,
308
+ secret,
309
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
310
+ active: true
311
+ };
312
+ this.registrations.set(id, registration);
313
+ logger.info({ webhookId: id, url }, "Webhook registered");
314
+ return registration;
315
+ }
316
+ /**
317
+ * Unregister a webhook by ID
318
+ *
319
+ * @returns true if found and removed, false if not found
320
+ */
321
+ unregister(id) {
322
+ const existed = this.registrations.delete(id);
323
+ if (existed) {
324
+ logger.info({ webhookId: id }, "Webhook unregistered");
325
+ }
326
+ return existed;
327
+ }
328
+ /**
329
+ * Get a registration by ID
330
+ */
331
+ get(id) {
332
+ return this.registrations.get(id);
333
+ }
334
+ /**
335
+ * Get all active registrations
336
+ */
337
+ getAll() {
338
+ return Array.from(this.registrations.values()).filter((r) => r.active);
339
+ }
340
+ /**
341
+ * Get all registrations (including inactive) for listing
342
+ */
343
+ getAllForList() {
344
+ return Array.from(this.registrations.values()).map((r) => ({
345
+ id: r.id,
346
+ url: r.url,
347
+ active: r.active,
348
+ createdAt: r.createdAt
349
+ }));
350
+ }
351
+ /**
352
+ * Current store size
353
+ */
354
+ get size() {
355
+ return this.registrations.size;
356
+ }
357
+ /**
358
+ * Clear all registrations (for testing)
359
+ */
360
+ clear() {
361
+ this.registrations.clear();
362
+ }
363
+ };
364
+ var webhookStore = new WebhookStore();
365
+
366
+ // src/routes/proof.ts
367
+ var import_express = require("express");
48
368
  var import_sdk2 = require("@sip-protocol/sdk");
369
+ var import_utils = require("@noble/hashes/utils");
49
370
 
50
371
  // src/middleware/error-handler.ts
51
372
  var import_sdk = require("@sip-protocol/sdk");
52
373
 
374
+ // src/monitoring/sentry.ts
375
+ var Sentry = __toESM(require("@sentry/node"));
376
+
377
+ // src/monitoring/metrics.ts
378
+ var import_prom_client = require("prom-client");
379
+ var register = new import_prom_client.Registry();
380
+ (0, import_prom_client.collectDefaultMetrics)({
381
+ register,
382
+ prefix: "sip_api_"
383
+ });
384
+ var httpRequestsTotal = new import_prom_client.Counter({
385
+ name: "sip_api_http_requests_total",
386
+ help: "Total number of HTTP requests",
387
+ labelNames: ["method", "path", "status"],
388
+ registers: [register]
389
+ });
390
+ var httpRequestDuration = new import_prom_client.Histogram({
391
+ name: "sip_api_http_request_duration_seconds",
392
+ help: "Duration of HTTP requests in seconds",
393
+ labelNames: ["method", "path", "status"],
394
+ buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
395
+ registers: [register]
396
+ });
397
+ var stealthAddressGenerations = new import_prom_client.Counter({
398
+ name: "sip_stealth_address_generations_total",
399
+ help: "Total number of stealth address generations",
400
+ labelNames: ["chain"],
401
+ registers: [register]
402
+ });
403
+ var commitmentCreations = new import_prom_client.Counter({
404
+ name: "sip_commitment_creations_total",
405
+ help: "Total number of commitment creations",
406
+ registers: [register]
407
+ });
408
+ var proofGenerations = new import_prom_client.Counter({
409
+ name: "sip_proof_generations_total",
410
+ help: "Total number of proof generations",
411
+ labelNames: ["type"],
412
+ registers: [register]
413
+ });
414
+ var proofGenerationDuration = new import_prom_client.Histogram({
415
+ name: "sip_proof_generation_duration_seconds",
416
+ help: "Duration of proof generation in seconds",
417
+ labelNames: ["type"],
418
+ buckets: [0.1, 0.5, 1, 2.5, 5, 10, 30, 60],
419
+ registers: [register]
420
+ });
421
+ var activeConnections = new import_prom_client.Gauge({
422
+ name: "sip_api_active_connections",
423
+ help: "Number of active connections",
424
+ registers: [register]
425
+ });
426
+ var swapRequests = new import_prom_client.Counter({
427
+ name: "sip_swap_requests_total",
428
+ help: "Total number of swap requests",
429
+ labelNames: ["from_chain", "to_chain", "status"],
430
+ registers: [register]
431
+ });
432
+ var quoteRequests = new import_prom_client.Counter({
433
+ name: "sip_quote_requests_total",
434
+ help: "Total number of quote requests",
435
+ labelNames: ["from_chain", "to_chain"],
436
+ registers: [register]
437
+ });
438
+
53
439
  // src/middleware/validation.ts
54
440
  var import_zod = require("zod");
441
+ var MAX_UINT256 = 2n ** 256n - 1n;
442
+ var amountSchema = import_zod.z.string().regex(/^[1-9]\d*$/, "Amount must be positive integer without leading zeros").max(78, "Amount exceeds maximum uint256 length").refine(
443
+ (v) => {
444
+ try {
445
+ const n = BigInt(v);
446
+ return n > 0n && n <= MAX_UINT256;
447
+ } catch {
448
+ return false;
449
+ }
450
+ },
451
+ { message: "Invalid amount: must be positive integer <= 2^256-1" }
452
+ );
453
+ var minAmountSchema = import_zod.z.string().regex(/^(0|[1-9]\d*)$/, "Amount must be non-negative integer without leading zeros").max(78, "Amount exceeds maximum uint256 length").refine(
454
+ (v) => {
455
+ try {
456
+ const n = BigInt(v);
457
+ return n >= 0n && n <= MAX_UINT256;
458
+ } catch {
459
+ return false;
460
+ }
461
+ },
462
+ { message: "Invalid amount: must be non-negative integer <= 2^256-1" }
463
+ );
464
+ function calculateMinAmount(input, slippageBps) {
465
+ if (slippageBps < 0 || slippageBps > 1e4) {
466
+ throw new Error("Invalid slippage: must be 0-10000 basis points");
467
+ }
468
+ const bps = BigInt(Math.floor(slippageBps));
469
+ const multiplier = 10000n - bps;
470
+ return input * multiplier / 10000n;
471
+ }
472
+ function percentToBps(percent) {
473
+ return Math.floor(percent * 100);
474
+ }
55
475
  function validateRequest(schema) {
56
476
  return async (req, res, next) => {
57
477
  try {
@@ -62,7 +482,9 @@ function validateRequest(schema) {
62
482
  req.query = await schema.query.parseAsync(req.query);
63
483
  }
64
484
  if (schema.params) {
65
- req.params = await schema.params.parseAsync(req.params);
485
+ req.params = await schema.params.parseAsync(
486
+ req.params
487
+ );
66
488
  }
67
489
  next();
68
490
  } catch (error) {
@@ -72,7 +494,8 @@ function validateRequest(schema) {
72
494
  error: {
73
495
  code: "VALIDATION_ERROR",
74
496
  message: "Invalid request data",
75
- details: error.errors
497
+ // Zod 4 renamed 'errors' to 'issues'
498
+ details: error.issues
76
499
  }
77
500
  });
78
501
  }
@@ -91,19 +514,19 @@ var schemas = {
91
514
  })
92
515
  }),
93
516
  createCommitment: import_zod.z.object({
94
- value: import_zod.z.string().regex(/^\d+$/),
95
- // bigint as string
517
+ value: amountSchema,
96
518
  blindingFactor: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/).optional()
97
519
  }),
98
520
  generateFundingProof: import_zod.z.object({
99
- balance: import_zod.z.string().regex(/^\d+$/),
100
- minRequired: import_zod.z.string().regex(/^\d+$/),
521
+ balance: amountSchema,
522
+ minRequired: minAmountSchema,
523
+ // Zero allowed: "prove I have >= 0" is valid
101
524
  balanceBlinding: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/)
102
525
  }),
103
526
  getQuote: import_zod.z.object({
104
527
  inputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
105
528
  inputToken: import_zod.z.string().min(1),
106
- inputAmount: import_zod.z.string().regex(/^\d+$/),
529
+ inputAmount: amountSchema,
107
530
  outputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
108
531
  outputToken: import_zod.z.string().min(1),
109
532
  slippageTolerance: import_zod.z.number().min(0).max(100).optional()
@@ -119,105 +542,269 @@ var schemas = {
119
542
  })
120
543
  };
121
544
 
122
- // src/routes/stealth.ts
123
- var router2 = (0, import_express2.Router)();
124
- router2.post(
125
- "/generate",
126
- validateRequest({ body: schemas.generateStealth }),
127
- async (req, res) => {
128
- const { chain, recipientMetaAddress } = req.body;
129
- const result = (0, import_sdk2.generateStealthAddress)(recipientMetaAddress);
130
- const response = {
131
- success: true,
132
- data: {
133
- stealthAddress: {
134
- address: result.stealthAddress.address,
135
- ephemeralPublicKey: result.stealthAddress.ephemeralPublicKey,
136
- viewTag: result.stealthAddress.viewTag
545
+ // src/middleware/rate-limit.ts
546
+ var import_express_rate_limit = __toESM(require("express-rate-limit"));
547
+ var import_rate_limit_redis = require("rate-limit-redis");
548
+ var import_ioredis = require("ioredis");
549
+ var WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000", 10);
550
+ var MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || "100", 10);
551
+ var SKIP_FAILED = process.env.RATE_LIMIT_SKIP_FAILED === "true";
552
+ var STORE_TYPE = process.env.RATE_LIMIT_STORE || "memory";
553
+ var REDIS_URL = process.env.REDIS_URL;
554
+ var redisClient = null;
555
+ var redisConnectionFailed = false;
556
+ function getRedisClient() {
557
+ if (redisConnectionFailed) {
558
+ return null;
559
+ }
560
+ if (!redisClient && REDIS_URL) {
561
+ redisClient = new import_ioredis.Redis(REDIS_URL, {
562
+ maxRetriesPerRequest: 3,
563
+ retryStrategy: (times) => {
564
+ if (times > 3) {
565
+ console.warn("[rate-limit] Redis connection failed, falling back to memory store");
566
+ redisConnectionFailed = true;
567
+ return null;
568
+ }
569
+ return Math.min(times * 100, 1e3);
570
+ },
571
+ lazyConnect: true
572
+ });
573
+ redisClient.on("error", (err) => {
574
+ console.warn("[rate-limit] Redis error:", err.message);
575
+ });
576
+ redisClient.on("connect", () => {
577
+ console.log("[rate-limit] Redis connected successfully");
578
+ });
579
+ }
580
+ return redisClient;
581
+ }
582
+ function getRedisStatus() {
583
+ const client = getRedisClient();
584
+ return {
585
+ storeType: client && !redisConnectionFailed ? "redis" : "memory",
586
+ configured: !!REDIS_URL,
587
+ connected: client?.status === "ready"
588
+ };
589
+ }
590
+ function createStore(prefix) {
591
+ if (STORE_TYPE === "redis" || REDIS_URL) {
592
+ const client = getRedisClient();
593
+ if (client && !redisConnectionFailed) {
594
+ const sendCommand = async (...args) => {
595
+ return client.call(args[0], ...args.slice(1));
596
+ };
597
+ return new import_rate_limit_redis.RedisStore({
598
+ sendCommand,
599
+ prefix: `rl:${prefix}:`
600
+ });
601
+ }
602
+ }
603
+ return void 0;
604
+ }
605
+ var rateLimiter = (0, import_express_rate_limit.default)({
606
+ windowMs: WINDOW_MS,
607
+ max: MAX_REQUESTS,
608
+ skipFailedRequests: SKIP_FAILED,
609
+ standardHeaders: true,
610
+ // Return rate limit info in `RateLimit-*` headers
611
+ legacyHeaders: false,
612
+ // Disable `X-RateLimit-*` headers
613
+ // Use Redis store if available, otherwise memory
614
+ store: createStore("api"),
615
+ handler: (_req, res) => {
616
+ res.status(429).json({
617
+ success: false,
618
+ error: {
619
+ code: "RATE_LIMIT_EXCEEDED",
620
+ message: "Too many requests, please try again later",
621
+ details: {
622
+ retryAfter: Math.ceil(WINDOW_MS / 1e3),
623
+ limit: MAX_REQUESTS,
624
+ windowMs: WINDOW_MS
137
625
  }
138
626
  }
139
- };
140
- res.json(response);
627
+ });
628
+ },
629
+ skip: (req) => {
630
+ return req.path === "/api/v1/health" || req.path === "/";
141
631
  }
142
- );
143
- var stealth_default = router2;
632
+ });
633
+ var strictRateLimiter = (0, import_express_rate_limit.default)({
634
+ windowMs: 6e4,
635
+ // 1 minute
636
+ max: 10,
637
+ // 10 requests per minute
638
+ skipFailedRequests: false,
639
+ standardHeaders: true,
640
+ legacyHeaders: false,
641
+ // Use Redis store if available with different prefix
642
+ store: createStore("strict"),
643
+ handler: (_req, res) => {
644
+ res.status(429).json({
645
+ success: false,
646
+ error: {
647
+ code: "RATE_LIMIT_EXCEEDED",
648
+ message: "Rate limit exceeded for sensitive endpoint",
649
+ details: {
650
+ retryAfter: 60,
651
+ limit: 10,
652
+ windowMs: 6e4
653
+ }
654
+ }
655
+ });
656
+ }
657
+ });
144
658
 
145
- // src/routes/commitment.ts
146
- var import_express3 = require("express");
147
- var import_sdk3 = require("@sip-protocol/sdk");
659
+ // src/middleware/auth.ts
660
+ var API_KEYS = (process.env.API_KEYS || "").split(",").filter(Boolean);
661
+ var NODE_ENV = process.env.NODE_ENV || "development";
662
+ var AUTH_ENABLED = process.env.AUTH_ENABLED !== "false" && NODE_ENV === "production";
663
+ var SKIP_PATHS = (process.env.AUTH_SKIP_PATHS || "/health,/,/webhooks/internal/helius").split(",").map((p) => p.trim());
148
664
 
149
- // ../../../../node_modules/@noble/hashes/esm/utils.js
150
- var hasHexBuiltin = /* @__PURE__ */ (() => (
151
- // @ts-ignore
152
- typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function"
153
- ))();
154
- var asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
155
- function asciiToBase16(ch) {
156
- if (ch >= asciis._0 && ch <= asciis._9)
157
- return ch - asciis._0;
158
- if (ch >= asciis.A && ch <= asciis.F)
159
- return ch - (asciis.A - 10);
160
- if (ch >= asciis.a && ch <= asciis.f)
161
- return ch - (asciis.a - 10);
162
- return;
665
+ // src/middleware/cors.ts
666
+ var import_cors = __toESM(require("cors"));
667
+ var NODE_ENV2 = process.env.NODE_ENV || "development";
668
+ var CORS_ORIGINS = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean) || [];
669
+ var CORS_CREDENTIALS = process.env.CORS_CREDENTIALS !== "false";
670
+ var CORS_MAX_AGE = parseInt(process.env.CORS_MAX_AGE || "86400", 10);
671
+ var DEV_ORIGINS = [
672
+ "http://localhost:3000",
673
+ "http://localhost:3001",
674
+ "http://localhost:4000",
675
+ "http://localhost:5173",
676
+ // Vite
677
+ "http://127.0.0.1:3000",
678
+ "http://127.0.0.1:3001",
679
+ "http://127.0.0.1:4000",
680
+ "http://127.0.0.1:5173"
681
+ ];
682
+ function getAllowedOrigins() {
683
+ if (CORS_ORIGINS.length > 0) {
684
+ return CORS_ORIGINS;
685
+ }
686
+ if (NODE_ENV2 === "development" || NODE_ENV2 === "test") {
687
+ return DEV_ORIGINS;
688
+ }
689
+ return [];
163
690
  }
164
- function hexToBytes(hex) {
165
- if (typeof hex !== "string")
166
- throw new Error("hex string expected, got " + typeof hex);
167
- if (hasHexBuiltin)
168
- return Uint8Array.fromHex(hex);
169
- const hl = hex.length;
170
- const al = hl / 2;
171
- if (hl % 2)
172
- throw new Error("hex string expected, got unpadded hex of length " + hl);
173
- const array = new Uint8Array(al);
174
- for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
175
- const n1 = asciiToBase16(hex.charCodeAt(hi));
176
- const n2 = asciiToBase16(hex.charCodeAt(hi + 1));
177
- if (n1 === void 0 || n2 === void 0) {
178
- const char = hex[hi] + hex[hi + 1];
179
- throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi);
691
+ function isOriginAllowed(origin) {
692
+ if (!origin) {
693
+ return true;
694
+ }
695
+ const allowedOrigins = getAllowedOrigins();
696
+ if (allowedOrigins.includes(origin)) {
697
+ return true;
698
+ }
699
+ let originUrl;
700
+ try {
701
+ originUrl = new URL(origin);
702
+ } catch {
703
+ console.warn(`[CORS] Rejected malformed origin: ${origin}`);
704
+ return false;
705
+ }
706
+ for (const allowed of allowedOrigins) {
707
+ if (allowed.startsWith("*.")) {
708
+ const baseDomain = allowed.slice(2);
709
+ const originHost = originUrl.host;
710
+ if (NODE_ENV2 === "production" && originUrl.protocol !== "https:") {
711
+ console.warn(`[CORS] Rejected non-HTTPS origin in production: ${origin}`);
712
+ continue;
713
+ }
714
+ if (originHost === baseDomain || originHost.endsWith("." + baseDomain)) {
715
+ return true;
716
+ }
180
717
  }
181
- array[ai] = n1 * 16 + n2;
182
718
  }
183
- return array;
719
+ return false;
184
720
  }
185
-
186
- // src/routes/commitment.ts
187
- var router3 = (0, import_express3.Router)();
188
- router3.post(
189
- "/create",
190
- validateRequest({ body: schemas.createCommitment }),
191
- async (req, res) => {
192
- const { value, blindingFactor } = req.body;
193
- const valueBigInt = BigInt(value);
194
- const blindingBytes = blindingFactor ? hexToBytes(blindingFactor) : void 0;
195
- const result = (0, import_sdk3.commit)(valueBigInt, blindingBytes);
196
- const response = {
197
- success: true,
198
- data: {
199
- commitment: result.commitment,
200
- blindingFactor: result.blinding
201
- }
202
- };
203
- res.json(response);
721
+ var corsOptionsDelegate = (req, callback) => {
722
+ const origin = req.headers.origin;
723
+ const allowed = isOriginAllowed(origin);
724
+ const options = {
725
+ origin: allowed ? origin : false,
726
+ credentials: CORS_CREDENTIALS,
727
+ maxAge: CORS_MAX_AGE,
728
+ methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
729
+ allowedHeaders: [
730
+ "Content-Type",
731
+ "Authorization",
732
+ "X-API-Key",
733
+ "X-Request-ID",
734
+ "X-Forwarded-For"
735
+ ],
736
+ exposedHeaders: [
737
+ "RateLimit-Limit",
738
+ "RateLimit-Remaining",
739
+ "RateLimit-Reset",
740
+ "X-Request-ID"
741
+ ]
742
+ };
743
+ if (!allowed && origin) {
744
+ console.warn(`[CORS] Blocked request from origin: ${origin}`);
204
745
  }
205
- );
206
- var commitment_default = router3;
746
+ callback(null, options);
747
+ };
748
+ var secureCors = (0, import_cors.default)(corsOptionsDelegate);
207
749
 
208
750
  // src/routes/proof.ts
209
- var import_express4 = require("express");
210
- var import_sdk4 = require("@sip-protocol/sdk");
211
- var router4 = (0, import_express4.Router)();
212
- var proofProvider = new import_sdk4.MockProofProvider();
213
- router4.post(
751
+ var router = (0, import_express.Router)();
752
+ var proofProvider = new import_sdk2.MockProofProvider();
753
+ var proofProviderReady = false;
754
+ var proofInitError = null;
755
+ var MAX_INIT_RETRIES = 3;
756
+ var RETRY_DELAY_MS = 2e3;
757
+ async function initializeProofProvider() {
758
+ for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
759
+ try {
760
+ await proofProvider.initialize();
761
+ proofProviderReady = true;
762
+ proofInitError = null;
763
+ logger.info({ attempt }, "Proof provider initialized successfully");
764
+ return;
765
+ } catch (err) {
766
+ proofInitError = err instanceof Error ? err : new Error(String(err));
767
+ logger.warn(
768
+ { attempt, maxRetries: MAX_INIT_RETRIES, error: proofInitError.message },
769
+ "Proof provider initialization failed, retrying..."
770
+ );
771
+ if (attempt < MAX_INIT_RETRIES) {
772
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt));
773
+ }
774
+ }
775
+ }
776
+ logger.error(
777
+ { error: proofInitError?.message },
778
+ "Proof provider initialization failed after all retries"
779
+ );
780
+ }
781
+ initializeProofProvider();
782
+ function isProofProviderReady() {
783
+ return proofProviderReady;
784
+ }
785
+ function getProofInitError() {
786
+ return proofInitError;
787
+ }
788
+ router.post(
214
789
  "/funding",
215
790
  validateRequest({ body: schemas.generateFundingProof }),
216
791
  async (req, res) => {
792
+ if (!proofProviderReady) {
793
+ const errorMsg = proofInitError?.message || "Proof provider is initializing";
794
+ logger.warn({ error: errorMsg }, "Proof request rejected - provider not ready");
795
+ return res.status(503).json({
796
+ success: false,
797
+ error: {
798
+ code: "PROOF_PROVIDER_NOT_READY",
799
+ message: "Proof generation service is not ready",
800
+ details: { reason: errorMsg }
801
+ }
802
+ });
803
+ }
217
804
  const { balance, minRequired, balanceBlinding } = req.body;
218
805
  const balanceBigInt = BigInt(balance);
219
806
  const minRequiredBigInt = BigInt(minRequired);
220
- const balanceBlindingBytes = hexToBytes(balanceBlinding);
807
+ const balanceBlindingBytes = (0, import_utils.hexToBytes)(balanceBlinding.replace(/^0x/, ""));
221
808
  const result = await proofProvider.generateFundingProof({
222
809
  balance: balanceBigInt,
223
810
  minimumRequired: minRequiredBigInt,
@@ -237,14 +824,109 @@ router4.post(
237
824
  res.json(response);
238
825
  }
239
826
  );
240
- var proof_default = router4;
827
+ var proof_default = router;
828
+
829
+ // src/routes/health.ts
830
+ var router2 = (0, import_express2.Router)();
831
+ var startTime = Date.now();
832
+ router2.get("/", (req, res) => {
833
+ const shuttingDown = isServerShuttingDown();
834
+ const status = shuttingDown ? "shutting_down" : "healthy";
835
+ const statusCode = shuttingDown ? 503 : 200;
836
+ const cacheMetrics = swapStore.getMetrics();
837
+ const proofReady = isProofProviderReady();
838
+ const proofError = getProofInitError();
839
+ const redisStatus = getRedisStatus();
840
+ const response = {
841
+ success: !shuttingDown,
842
+ data: {
843
+ status,
844
+ version: process.env.npm_package_version || "0.1.0",
845
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
846
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
847
+ services: {
848
+ proofProvider: {
849
+ ready: proofReady,
850
+ error: proofError?.message || null
851
+ },
852
+ rateLimiter: {
853
+ store: redisStatus.storeType,
854
+ redisConfigured: redisStatus.configured,
855
+ redisConnected: redisStatus.connected
856
+ }
857
+ },
858
+ cache: {
859
+ swaps: {
860
+ size: cacheMetrics.size,
861
+ maxSize: cacheMetrics.maxSize,
862
+ utilizationPercent: cacheMetrics.utilizationPercent
863
+ }
864
+ }
865
+ }
866
+ };
867
+ res.status(statusCode).json(response);
868
+ });
869
+ var health_default = router2;
870
+
871
+ // src/routes/stealth.ts
872
+ var import_express3 = require("express");
873
+ var import_sdk3 = require("@sip-protocol/sdk");
874
+ var router3 = (0, import_express3.Router)();
875
+ router3.post(
876
+ "/generate",
877
+ validateRequest({ body: schemas.generateStealth }),
878
+ async (req, res) => {
879
+ const { chain, recipientMetaAddress } = req.body;
880
+ const result = (0, import_sdk3.generateStealthAddress)(recipientMetaAddress);
881
+ const response = {
882
+ success: true,
883
+ data: {
884
+ stealthAddress: {
885
+ address: result.stealthAddress.address,
886
+ ephemeralPublicKey: result.stealthAddress.ephemeralPublicKey,
887
+ viewTag: result.stealthAddress.viewTag
888
+ }
889
+ }
890
+ };
891
+ res.json(response);
892
+ }
893
+ );
894
+ var stealth_default = router3;
895
+
896
+ // src/routes/commitment.ts
897
+ var import_express4 = require("express");
898
+ var import_sdk4 = require("@sip-protocol/sdk");
899
+ var import_utils2 = require("@noble/hashes/utils");
900
+ var router4 = (0, import_express4.Router)();
901
+ router4.post(
902
+ "/create",
903
+ validateRequest({ body: schemas.createCommitment }),
904
+ async (req, res) => {
905
+ const { value, blindingFactor } = req.body;
906
+ const valueBigInt = BigInt(value);
907
+ const blindingBytes = blindingFactor ? (0, import_utils2.hexToBytes)(blindingFactor.replace(/^0x/, "")) : void 0;
908
+ const result = (0, import_sdk4.commit)(valueBigInt, blindingBytes);
909
+ const response = {
910
+ success: true,
911
+ data: {
912
+ commitment: result.commitment,
913
+ blindingFactor: result.blinding
914
+ }
915
+ };
916
+ res.json(response);
917
+ }
918
+ );
919
+ var commitment_default = router4;
241
920
 
242
921
  // src/routes/swap.ts
243
922
  var import_express5 = require("express");
244
923
  var import_sdk5 = require("@sip-protocol/sdk");
245
924
  var router5 = (0, import_express5.Router)();
246
925
  var sip = new import_sdk5.SIP({ network: "testnet" });
247
- var swaps = /* @__PURE__ */ new Map();
926
+ var MOCK_MODE_ENABLED = env.NODE_ENV !== "production";
927
+ if (!MOCK_MODE_ENABLED) {
928
+ logger.warn("Production mode: Mock quotes and swaps are DISABLED");
929
+ }
248
930
  router5.post(
249
931
  "/",
250
932
  validateRequest({ body: schemas.getQuote }),
@@ -257,37 +939,66 @@ router5.post(
257
939
  outputToken,
258
940
  slippageTolerance
259
941
  } = req.body;
942
+ if (!MOCK_MODE_ENABLED) {
943
+ return res.status(503).json({
944
+ success: false,
945
+ error: {
946
+ code: "QUOTE_SERVICE_UNAVAILABLE",
947
+ message: "Real quote aggregator not configured. This API is not ready for production use.",
948
+ details: {
949
+ hint: "Configure QUOTE_AGGREGATOR_URL or use development mode"
950
+ }
951
+ }
952
+ });
953
+ }
954
+ if (!(0, import_sdk5.isKnownToken)(inputToken, inputChain)) {
955
+ return res.status(400).json({
956
+ success: false,
957
+ error: {
958
+ code: "UNKNOWN_TOKEN",
959
+ message: `Unknown input token: ${inputToken} on ${inputChain}`
960
+ }
961
+ });
962
+ }
963
+ if (!(0, import_sdk5.isKnownToken)(outputToken, outputChain)) {
964
+ return res.status(400).json({
965
+ success: false,
966
+ error: {
967
+ code: "UNKNOWN_TOKEN",
968
+ message: `Unknown output token: ${outputToken} on ${outputChain}`
969
+ }
970
+ });
971
+ }
972
+ const inputAsset = (0, import_sdk5.getAsset)(inputToken, inputChain);
973
+ const outputAsset = (0, import_sdk5.getAsset)(outputToken, outputChain);
974
+ const inputAmountBigInt = BigInt(inputAmount);
975
+ const slippagePercent = slippageTolerance ?? 1;
976
+ const slippageBps = percentToBps(slippagePercent);
260
977
  const intent = await sip.createIntent({
261
978
  input: {
262
- asset: {
263
- chain: inputChain,
264
- address: null,
265
- // Native token
266
- symbol: inputToken,
267
- decimals: 9
268
- },
269
- amount: BigInt(inputAmount)
979
+ asset: inputAsset,
980
+ amount: inputAmountBigInt
270
981
  },
271
982
  output: {
272
- asset: {
273
- chain: outputChain,
274
- address: null,
275
- // Native token
276
- symbol: outputToken,
277
- decimals: 9
278
- },
279
- minAmount: BigInt(inputAmount) * 95n / 100n,
280
- // 5% slippage
281
- maxSlippage: (slippageTolerance || 1) / 100
983
+ asset: outputAsset,
984
+ minAmount: calculateMinAmount(inputAmountBigInt, slippageBps),
985
+ maxSlippage: slippagePercent / 100
282
986
  },
283
987
  privacy: import_sdk5.PrivacyLevel.TRANSPARENT
284
988
  // Default to transparent for quote
285
989
  });
990
+ logger.warn({
991
+ inputChain,
992
+ inputToken,
993
+ outputChain,
994
+ outputToken,
995
+ inputAmount
996
+ }, "Returning MOCK quote - not for production use");
286
997
  const mockQuote = {
287
998
  quoteId: `quote-${Date.now()}`,
288
999
  inputAmount,
1000
+ // Mock: 5% fee (clearly fake rate)
289
1001
  outputAmount: (BigInt(inputAmount) * 95n / 100n).toString(),
290
- // Mock 5% fee
291
1002
  rate: "0.95",
292
1003
  estimatedTime: 30,
293
1004
  fees: {
@@ -307,7 +1018,10 @@ router5.post(
307
1018
  };
308
1019
  const response = {
309
1020
  success: true,
310
- data: mockQuote
1021
+ data: {
1022
+ ...mockQuote,
1023
+ _warning: "MOCK_DATA: This quote uses simulated pricing. Do not use for real transactions."
1024
+ }
311
1025
  };
312
1026
  res.json(response);
313
1027
  }
@@ -316,23 +1030,45 @@ router5.post(
316
1030
  "/swap",
317
1031
  validateRequest({ body: schemas.executeSwap }),
318
1032
  async (req, res) => {
319
- const { intentId, quoteId, privacy, viewingKey } = req.body;
1033
+ const { intentId, quoteId, inputAmount } = req.body;
1034
+ if (!MOCK_MODE_ENABLED) {
1035
+ return res.status(503).json({
1036
+ success: false,
1037
+ error: {
1038
+ code: "SWAP_SERVICE_UNAVAILABLE",
1039
+ message: "Real swap executor not configured. This API is not ready for production use.",
1040
+ details: {
1041
+ hint: "Configure SWAP_EXECUTOR_URL or use development mode"
1042
+ }
1043
+ }
1044
+ });
1045
+ }
320
1046
  const swapId = `swap-${Date.now()}`;
1047
+ const actualInputAmount = inputAmount || "0";
1048
+ if (!inputAmount) {
1049
+ logger.warn({ swapId, quoteId, intentId }, "Swap created without inputAmount - using 0");
1050
+ }
321
1051
  const swap = {
322
1052
  id: swapId,
323
1053
  status: "pending",
324
- inputAmount: "1000000000",
325
- // Mock value
1054
+ inputAmount: actualInputAmount,
326
1055
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
327
1056
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
328
1057
  };
329
- swaps.set(swapId, swap);
1058
+ swapStore.set(swapId, swap);
1059
+ logger.warn({
1060
+ swapId,
1061
+ quoteId,
1062
+ intentId,
1063
+ inputAmount: actualInputAmount
1064
+ }, "Creating MOCK swap - not for production use");
330
1065
  const response = {
331
1066
  success: true,
332
1067
  data: {
333
1068
  swapId,
334
1069
  status: "pending",
335
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1070
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1071
+ _warning: "MOCK_DATA: This swap is simulated. No real transaction will be executed."
336
1072
  }
337
1073
  };
338
1074
  res.json(response);
@@ -343,13 +1079,14 @@ router5.get(
343
1079
  validateRequest({ params: schemas.swapStatus }),
344
1080
  async (req, res) => {
345
1081
  const { id } = req.params;
346
- const swap = swaps.get(id);
1082
+ const swapId = Array.isArray(id) ? id[0] : id;
1083
+ const swap = swapStore.get(swapId);
347
1084
  if (!swap) {
348
1085
  return res.status(404).json({
349
1086
  success: false,
350
1087
  error: {
351
1088
  code: "SWAP_NOT_FOUND",
352
- message: `Swap ${id} not found`
1089
+ message: `Swap ${swapId} not found`
353
1090
  }
354
1091
  });
355
1092
  }
@@ -362,17 +1099,299 @@ router5.get(
362
1099
  );
363
1100
  var swap_default = router5;
364
1101
 
365
- // src/routes/index.ts
1102
+ // src/routes/webhook.ts
1103
+ var import_express6 = require("express");
1104
+ var import_zod2 = require("zod");
1105
+
1106
+ // src/services/helius-listener.ts
1107
+ var import_sdk6 = require("@sip-protocol/sdk");
1108
+
1109
+ // src/services/webhook-delivery.ts
1110
+ var import_hmac = require("@noble/hashes/hmac");
1111
+ var import_sha256 = require("@noble/hashes/sha256");
1112
+ var import_utils3 = require("@noble/hashes/utils");
1113
+ function computeHmacSignature(secret, body) {
1114
+ const encoder = new TextEncoder();
1115
+ const sig = (0, import_utils3.bytesToHex)((0, import_hmac.hmac)(import_sha256.sha256, encoder.encode(secret), encoder.encode(body)));
1116
+ return `sha256=${sig}`;
1117
+ }
1118
+ function sleep(ms) {
1119
+ return new Promise((resolve) => setTimeout(resolve, ms));
1120
+ }
1121
+ var WebhookDeliveryService = class {
1122
+ maxRetries;
1123
+ pendingDeliveries = /* @__PURE__ */ new Set();
1124
+ constructor(maxRetries) {
1125
+ this.maxRetries = maxRetries ?? env.WEBHOOK_DELIVERY_MAX_RETRIES;
1126
+ }
1127
+ /**
1128
+ * Build the delivery payload from a scan result
1129
+ */
1130
+ buildPayload(registration, payment) {
1131
+ return {
1132
+ event: "payment.received",
1133
+ webhookId: registration.id,
1134
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1135
+ data: {
1136
+ txSignature: payment.txSignature,
1137
+ stealthAddress: payment.stealthAddress,
1138
+ ephemeralPublicKey: payment.ephemeralPublicKey,
1139
+ amount: payment.amount.toString(),
1140
+ mint: payment.mint,
1141
+ tokenSymbol: payment.tokenSymbol,
1142
+ slot: payment.slot,
1143
+ blockTime: payment.timestamp
1144
+ }
1145
+ };
1146
+ }
1147
+ /**
1148
+ * Deliver a payment notification to a registered webhook
1149
+ *
1150
+ * Retries with exponential backoff on failure.
1151
+ */
1152
+ async deliver(registration, payment) {
1153
+ const payload = this.buildPayload(registration, payment);
1154
+ const body = JSON.stringify(payload);
1155
+ const signature = computeHmacSignature(registration.secret, body);
1156
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
1157
+ try {
1158
+ const response = await fetch(registration.url, {
1159
+ method: "POST",
1160
+ headers: {
1161
+ "Content-Type": "application/json",
1162
+ "X-SIP-Signature": signature,
1163
+ "X-SIP-Webhook-Id": registration.id
1164
+ },
1165
+ body,
1166
+ signal: AbortSignal.timeout(1e4)
1167
+ });
1168
+ if (response.ok) {
1169
+ logger.info(
1170
+ { webhookId: registration.id, attempt, status: response.status },
1171
+ "Webhook delivered"
1172
+ );
1173
+ return true;
1174
+ }
1175
+ logger.warn(
1176
+ { webhookId: registration.id, attempt, status: response.status },
1177
+ "Webhook delivery failed (non-2xx)"
1178
+ );
1179
+ } catch (error) {
1180
+ logger.warn(
1181
+ { webhookId: registration.id, attempt, error: error.message },
1182
+ "Webhook delivery error"
1183
+ );
1184
+ }
1185
+ if (attempt < this.maxRetries) {
1186
+ const backoffMs = Math.pow(4, attempt) * 1e3;
1187
+ await sleep(backoffMs);
1188
+ }
1189
+ }
1190
+ logger.error(
1191
+ { webhookId: registration.id, maxRetries: this.maxRetries },
1192
+ "Webhook delivery exhausted all retries"
1193
+ );
1194
+ return false;
1195
+ }
1196
+ /**
1197
+ * Queue a delivery (fire-and-forget with tracking)
1198
+ */
1199
+ queueDelivery(registration, payment) {
1200
+ const promise = this.deliver(registration, payment).then(() => {
1201
+ }).catch((err) => {
1202
+ logger.error({ webhookId: registration.id, err }, "Unhandled webhook delivery error");
1203
+ }).finally(() => {
1204
+ this.pendingDeliveries.delete(promise);
1205
+ });
1206
+ this.pendingDeliveries.add(promise);
1207
+ }
1208
+ /**
1209
+ * Wait for all pending deliveries to complete (graceful shutdown)
1210
+ */
1211
+ async drainPending() {
1212
+ if (this.pendingDeliveries.size > 0) {
1213
+ logger.info({ pending: this.pendingDeliveries.size }, "Draining pending webhook deliveries");
1214
+ await Promise.allSettled(this.pendingDeliveries);
1215
+ logger.info("All webhook deliveries drained");
1216
+ }
1217
+ }
1218
+ };
1219
+ var webhookDeliveryService = new WebhookDeliveryService();
1220
+
1221
+ // src/services/helius-listener.ts
1222
+ var HeliusListenerService = class {
1223
+ /**
1224
+ * Process an incoming Helius webhook payload
1225
+ *
1226
+ * 1. Verify Helius signature (if configured)
1227
+ * 2. For each active registration, check if payment is for them
1228
+ * 3. On match, queue delivery to the agent's URL
1229
+ *
1230
+ * @returns Number of matched deliveries queued
1231
+ */
1232
+ async processIncoming(payload, headers) {
1233
+ if (env.HELIUS_WEBHOOK_SECRET && headers.rawBody) {
1234
+ const valid = (0, import_sdk6.verifyWebhookSignature)(
1235
+ headers.rawBody,
1236
+ headers.signature,
1237
+ env.HELIUS_WEBHOOK_SECRET
1238
+ );
1239
+ if (!valid) {
1240
+ logger.warn("Helius webhook signature verification failed");
1241
+ throw new Error("HELIUS_SIGNATURE_INVALID");
1242
+ }
1243
+ }
1244
+ const transactions = Array.isArray(payload) ? payload : [payload];
1245
+ const registrations = webhookStore.getAll();
1246
+ if (registrations.length === 0) {
1247
+ logger.debug("No active webhook registrations, skipping processing");
1248
+ return 0;
1249
+ }
1250
+ let matchCount = 0;
1251
+ for (const tx of transactions) {
1252
+ for (const registration of registrations) {
1253
+ try {
1254
+ const payment = await (0, import_sdk6.processWebhookTransaction)(
1255
+ tx,
1256
+ registration.viewingPrivateKey,
1257
+ registration.spendingPublicKey
1258
+ );
1259
+ if (payment) {
1260
+ matchCount++;
1261
+ webhookDeliveryService.queueDelivery(registration, payment);
1262
+ logger.info(
1263
+ {
1264
+ webhookId: registration.id,
1265
+ txSignature: payment.txSignature
1266
+ },
1267
+ "Payment matched, delivery queued"
1268
+ );
1269
+ }
1270
+ } catch (error) {
1271
+ logger.warn(
1272
+ {
1273
+ webhookId: registration.id,
1274
+ error: error.message
1275
+ },
1276
+ "Error checking transaction against registration"
1277
+ );
1278
+ }
1279
+ }
1280
+ }
1281
+ logger.info(
1282
+ {
1283
+ transactions: transactions.length,
1284
+ registrations: registrations.length,
1285
+ matches: matchCount
1286
+ },
1287
+ "Helius webhook processed"
1288
+ );
1289
+ return matchCount;
1290
+ }
1291
+ };
1292
+ var heliusListenerService = new HeliusListenerService();
1293
+
1294
+ // src/routes/webhook.ts
366
1295
  var router6 = (0, import_express6.Router)();
367
- router6.use("/health", health_default);
368
- router6.use("/stealth", stealth_default);
369
- router6.use("/commitment", commitment_default);
370
- router6.use("/proof", proof_default);
371
- router6.use("/quote", swap_default);
372
- router6.use("/swap", swap_default);
373
- var routes_default = router6;
374
- /*! Bundled license information:
1296
+ var webhookSchemas = {
1297
+ register: import_zod2.z.object({
1298
+ url: import_zod2.z.string().url("Must be a valid URL"),
1299
+ viewingPrivateKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex"),
1300
+ spendingPublicKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex")
1301
+ }),
1302
+ unregister: import_zod2.z.object({
1303
+ id: import_zod2.z.string().min(1)
1304
+ })
1305
+ };
1306
+ router6.post(
1307
+ "/register",
1308
+ validateRequest({ body: webhookSchemas.register }),
1309
+ (req, res) => {
1310
+ const { url, viewingPrivateKey, spendingPublicKey } = req.body;
1311
+ try {
1312
+ const registration = webhookStore.register(url, viewingPrivateKey, spendingPublicKey);
1313
+ const response = {
1314
+ success: true,
1315
+ data: {
1316
+ id: registration.id,
1317
+ url: registration.url,
1318
+ secret: registration.secret,
1319
+ createdAt: registration.createdAt
1320
+ }
1321
+ };
1322
+ res.status(201).json(response);
1323
+ } catch (error) {
1324
+ if (error.message === "WEBHOOK_STORE_FULL") {
1325
+ res.status(503).json({
1326
+ success: false,
1327
+ error: {
1328
+ code: "WEBHOOK_STORE_FULL",
1329
+ message: "Maximum webhook registrations reached"
1330
+ }
1331
+ });
1332
+ return;
1333
+ }
1334
+ throw error;
1335
+ }
1336
+ }
1337
+ );
1338
+ router6.delete(
1339
+ "/:id",
1340
+ validateRequest({ params: webhookSchemas.unregister }),
1341
+ (req, res) => {
1342
+ const removed = webhookStore.unregister(req.params.id);
1343
+ if (!removed) {
1344
+ res.status(404).json({
1345
+ success: false,
1346
+ error: {
1347
+ code: "WEBHOOK_NOT_FOUND",
1348
+ message: "Webhook not found"
1349
+ }
1350
+ });
1351
+ return;
1352
+ }
1353
+ res.status(204).send();
1354
+ }
1355
+ );
1356
+ router6.get("/", (_req, res) => {
1357
+ const webhooks = webhookStore.getAllForList();
1358
+ const response = {
1359
+ success: true,
1360
+ data: { webhooks }
1361
+ };
1362
+ res.json(response);
1363
+ });
1364
+ router6.post("/internal/helius", async (req, res) => {
1365
+ const headers = {
1366
+ signature: req.headers["x-helius-signature"],
1367
+ rawBody: typeof req.body === "string" ? req.body : JSON.stringify(req.body)
1368
+ };
1369
+ try {
1370
+ const matchCount = await heliusListenerService.processIncoming(req.body, headers);
1371
+ res.status(200).json({ success: true, matched: matchCount });
1372
+ } catch (error) {
1373
+ if (error.message === "HELIUS_SIGNATURE_INVALID") {
1374
+ res.status(401).json({
1375
+ success: false,
1376
+ error: {
1377
+ code: "HELIUS_SIGNATURE_INVALID",
1378
+ message: "Invalid Helius webhook signature"
1379
+ }
1380
+ });
1381
+ return;
1382
+ }
1383
+ throw error;
1384
+ }
1385
+ });
1386
+ var webhook_default = router6;
375
1387
 
376
- @noble/hashes/esm/utils.js:
377
- (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
378
- */
1388
+ // src/routes/index.ts
1389
+ var router7 = (0, import_express7.Router)();
1390
+ router7.use("/health", health_default);
1391
+ router7.use("/stealth", stealth_default);
1392
+ router7.use("/commitment", commitment_default);
1393
+ router7.use("/proof", proof_default);
1394
+ router7.use("/quote", swap_default);
1395
+ router7.use("/swap", swap_default);
1396
+ router7.use("/webhooks", webhook_default);
1397
+ var routes_default = router7;