@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.
package/dist/server.js CHANGED
@@ -30,69 +30,615 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/server.ts
31
31
  var server_exports = {};
32
32
  __export(server_exports, {
33
- default: () => server_default
33
+ default: () => server_default,
34
+ isServerShuttingDown: () => isServerShuttingDown
34
35
  });
35
36
  module.exports = __toCommonJS(server_exports);
36
- var import_express7 = __toESM(require("express"));
37
- var import_cors = __toESM(require("cors"));
37
+ var import_express9 = __toESM(require("express"));
38
38
  var import_helmet = __toESM(require("helmet"));
39
39
  var import_compression = __toESM(require("compression"));
40
- var import_morgan = __toESM(require("morgan"));
41
40
 
42
41
  // src/routes/index.ts
43
- var import_express6 = require("express");
42
+ var import_express7 = require("express");
44
43
 
45
44
  // src/routes/health.ts
46
- var import_express = require("express");
47
- var router = (0, import_express.Router)();
48
- var startTime = Date.now();
49
- router.get("/", (req, res) => {
50
- const response = {
51
- success: true,
52
- data: {
53
- status: "healthy",
54
- version: process.env.npm_package_version || "0.1.0",
55
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
56
- uptime: Math.floor((Date.now() - startTime) / 1e3)
45
+ var import_express2 = require("express");
46
+
47
+ // src/logger.ts
48
+ var import_pino = __toESM(require("pino"));
49
+ var import_pino_http = __toESM(require("pino-http"));
50
+ var import_crypto = __toESM(require("crypto"));
51
+
52
+ // src/config.ts
53
+ var import_envalid = require("envalid");
54
+ var env = (0, import_envalid.cleanEnv)(process.env, {
55
+ // Server configuration
56
+ NODE_ENV: (0, import_envalid.str)({
57
+ choices: ["development", "production", "test"],
58
+ default: "development",
59
+ desc: "Application environment"
60
+ }),
61
+ PORT: (0, import_envalid.port)({
62
+ default: 3e3,
63
+ desc: "Server port"
64
+ }),
65
+ // CORS configuration
66
+ CORS_ORIGINS: (0, import_envalid.str)({
67
+ default: "",
68
+ desc: "Comma-separated list of allowed origins (empty = localhost only in dev)"
69
+ }),
70
+ // Authentication
71
+ API_KEYS: (0, import_envalid.str)({
72
+ default: "",
73
+ desc: "Comma-separated list of valid API keys (empty = auth disabled in dev)"
74
+ }),
75
+ // Rate limiting
76
+ RATE_LIMIT_MAX: (0, import_envalid.num)({
77
+ default: 100,
78
+ desc: "Maximum requests per window"
79
+ }),
80
+ RATE_LIMIT_WINDOW_MS: (0, import_envalid.num)({
81
+ default: 6e4,
82
+ desc: "Rate limit window in milliseconds"
83
+ }),
84
+ // Proxy trust configuration (for X-Forwarded-For header)
85
+ // Set to number of trusted proxies (e.g., 1 for single nginx)
86
+ // or 'loopback' for local proxies, or 'uniquelocal' for private IPs
87
+ TRUST_PROXY: (0, import_envalid.str)({
88
+ default: "1",
89
+ desc: 'Express trust proxy setting (number of hops, "loopback", "uniquelocal", or "false")'
90
+ }),
91
+ // Logging
92
+ LOG_LEVEL: (0, import_envalid.str)({
93
+ choices: ["trace", "debug", "info", "warn", "error", "fatal"],
94
+ default: "info",
95
+ desc: "Logging level"
96
+ }),
97
+ // Graceful shutdown
98
+ SHUTDOWN_TIMEOUT_MS: (0, import_envalid.num)({
99
+ default: 3e4,
100
+ desc: "Graceful shutdown timeout in milliseconds"
101
+ }),
102
+ // Monitoring
103
+ SENTRY_DSN: (0, import_envalid.str)({
104
+ default: "",
105
+ desc: "Sentry DSN for error tracking (optional)"
106
+ }),
107
+ METRICS_ENABLED: (0, import_envalid.str)({
108
+ choices: ["true", "false"],
109
+ default: "true",
110
+ desc: "Enable Prometheus metrics endpoint"
111
+ }),
112
+ // Webhook configuration
113
+ HELIUS_WEBHOOK_SECRET: (0, import_envalid.str)({
114
+ default: "",
115
+ desc: "Helius webhook HMAC secret for signature verification (optional)"
116
+ }),
117
+ WEBHOOK_DELIVERY_MAX_RETRIES: (0, import_envalid.num)({
118
+ default: 3,
119
+ desc: "Maximum delivery attempts for webhook notifications"
120
+ }),
121
+ WEBHOOK_STORE_MAX_SIZE: (0, import_envalid.num)({
122
+ default: 1e3,
123
+ desc: "Maximum number of registered webhooks"
124
+ })
125
+ });
126
+ function logConfigWarnings(logger2) {
127
+ if (env.isProduction) {
128
+ if (!env.API_KEYS) {
129
+ logger2.warn("API_KEYS not set in production - authentication disabled");
130
+ }
131
+ if (!env.CORS_ORIGINS) {
132
+ logger2.warn("CORS_ORIGINS not set in production - only localhost allowed");
133
+ }
134
+ }
135
+ }
136
+ var isProduction = env.isProduction;
137
+ var isDevelopment = env.isDevelopment;
138
+ var isTest = env.isTest;
139
+
140
+ // src/logger.ts
141
+ var loggerConfig = {
142
+ level: env.LOG_LEVEL,
143
+ base: {
144
+ service: "sip-api",
145
+ version: "0.1.0"
146
+ },
147
+ timestamp: import_pino.default.stdTimeFunctions.isoTime
148
+ };
149
+ if (env.isDevelopment) {
150
+ loggerConfig.transport = {
151
+ target: "pino-pretty",
152
+ options: {
153
+ colorize: true,
154
+ translateTime: "HH:MM:ss",
155
+ ignore: "pid,hostname,service,version"
57
156
  }
58
157
  };
59
- res.json(response);
158
+ }
159
+ var logger = (0, import_pino.default)(loggerConfig);
160
+ var requestLogger = (0, import_pino_http.default)({
161
+ logger,
162
+ // Use requestId from requestIdMiddleware (set earlier in chain)
163
+ // Falls back to header or generates new if middleware didn't run
164
+ genReqId: (req) => {
165
+ const augmentedReq = req;
166
+ if (augmentedReq.requestId) {
167
+ return augmentedReq.requestId;
168
+ }
169
+ const existingId = req.headers["x-request-id"];
170
+ if (existingId && typeof existingId === "string") {
171
+ return existingId;
172
+ }
173
+ return import_crypto.default.randomUUID();
174
+ },
175
+ customLogLevel: (_req, res, err) => {
176
+ if (res.statusCode >= 500 || err) return "error";
177
+ if (res.statusCode >= 400) return "warn";
178
+ return "info";
179
+ },
180
+ customSuccessMessage: (req, res) => {
181
+ return `${req.method} ${req.url} ${res.statusCode}`;
182
+ },
183
+ customErrorMessage: (req, res, err) => {
184
+ return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
185
+ },
186
+ // Don't log health checks in production
187
+ autoLogging: {
188
+ ignore: (req) => {
189
+ return req.url === "/api/v1/health" && env.isProduction;
190
+ }
191
+ }
60
192
  });
61
- var health_default = router;
62
193
 
63
- // src/routes/stealth.ts
64
- var import_express2 = require("express");
194
+ // src/shutdown.ts
195
+ var isShuttingDown = false;
196
+ function isServerShuttingDown() {
197
+ return isShuttingDown;
198
+ }
199
+ function setupGracefulShutdown(server, cleanup) {
200
+ const shutdown = async (signal) => {
201
+ if (isShuttingDown) {
202
+ logger.warn({ signal }, "Shutdown already in progress, ignoring signal");
203
+ return;
204
+ }
205
+ isShuttingDown = true;
206
+ logger.info({ signal }, "Received shutdown signal, starting graceful shutdown...");
207
+ server.close(async (err) => {
208
+ if (err) {
209
+ logger.error({ err }, "Error closing HTTP server");
210
+ process.exit(1);
211
+ }
212
+ logger.info("HTTP server closed, no longer accepting connections");
213
+ if (cleanup) {
214
+ try {
215
+ logger.info("Running cleanup tasks...");
216
+ await cleanup();
217
+ logger.info("Cleanup completed successfully");
218
+ } catch (cleanupErr) {
219
+ logger.error({ err: cleanupErr }, "Error during cleanup");
220
+ }
221
+ }
222
+ logger.info("Graceful shutdown complete, exiting");
223
+ process.exit(0);
224
+ });
225
+ setTimeout(() => {
226
+ logger.error(
227
+ { timeoutMs: env.SHUTDOWN_TIMEOUT_MS },
228
+ "Graceful shutdown timeout exceeded, forcing exit"
229
+ );
230
+ process.exit(1);
231
+ }, env.SHUTDOWN_TIMEOUT_MS);
232
+ };
233
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
234
+ process.on("SIGINT", () => shutdown("SIGINT"));
235
+ process.on("uncaughtException", (err) => {
236
+ logger.fatal({ err }, "Uncaught exception, shutting down");
237
+ shutdown("uncaughtException");
238
+ });
239
+ process.on("unhandledRejection", (reason) => {
240
+ logger.fatal({ reason }, "Unhandled rejection, shutting down");
241
+ shutdown("unhandledRejection");
242
+ });
243
+ logger.debug("Graceful shutdown handlers registered");
244
+ }
245
+ function shutdownMiddleware(req, res, next) {
246
+ if (isShuttingDown) {
247
+ if (req.path === "/api/v1/health") {
248
+ return next();
249
+ }
250
+ res.status(503).json({
251
+ success: false,
252
+ error: {
253
+ code: "SERVICE_UNAVAILABLE",
254
+ message: "Server is shutting down"
255
+ }
256
+ });
257
+ return;
258
+ }
259
+ next();
260
+ }
261
+
262
+ // src/stores/swap-store.ts
263
+ var import_lru_cache = require("lru-cache");
264
+ var DEFAULT_MAX_SIZE = 1e4;
265
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
266
+ var SwapStore = class {
267
+ cache;
268
+ maxSize;
269
+ ttlMs;
270
+ constructor(config = {}) {
271
+ this.maxSize = config.maxSize ?? DEFAULT_MAX_SIZE;
272
+ this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
273
+ this.cache = new import_lru_cache.LRUCache({
274
+ max: this.maxSize,
275
+ ttl: this.ttlMs,
276
+ updateAgeOnGet: false,
277
+ // Don't reset TTL on read
278
+ updateAgeOnHas: false,
279
+ // Log when items are evicted or expired
280
+ disposeAfter: (_value, key, reason) => {
281
+ if (reason === "evict") {
282
+ logger.debug({ swapId: key, reason }, "Swap evicted from cache (LRU)");
283
+ } else if (reason === "expire") {
284
+ logger.debug({ swapId: key, reason }, "Swap expired from cache (TTL)");
285
+ }
286
+ }
287
+ });
288
+ logger.info(
289
+ { maxSize: this.maxSize, ttlMs: this.ttlMs },
290
+ "SwapStore initialized"
291
+ );
292
+ }
293
+ /**
294
+ * Get a swap by ID
295
+ */
296
+ get(id) {
297
+ return this.cache.get(id);
298
+ }
299
+ /**
300
+ * Check if a swap exists
301
+ */
302
+ has(id) {
303
+ return this.cache.has(id);
304
+ }
305
+ /**
306
+ * Set/update a swap
307
+ */
308
+ set(id, data) {
309
+ this.cache.set(id, data);
310
+ }
311
+ /**
312
+ * Delete a swap
313
+ */
314
+ delete(id) {
315
+ return this.cache.delete(id);
316
+ }
317
+ /**
318
+ * Update swap status
319
+ */
320
+ updateStatus(id, status, updates) {
321
+ const existing = this.cache.get(id);
322
+ if (!existing) {
323
+ return void 0;
324
+ }
325
+ const updated = {
326
+ ...existing,
327
+ ...updates,
328
+ status,
329
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
330
+ };
331
+ this.cache.set(id, updated);
332
+ return updated;
333
+ }
334
+ /**
335
+ * Get store metrics for monitoring
336
+ */
337
+ getMetrics() {
338
+ const size = this.cache.size;
339
+ return {
340
+ size,
341
+ maxSize: this.maxSize,
342
+ ttlMs: this.ttlMs,
343
+ utilizationPercent: Math.round(size / this.maxSize * 100)
344
+ };
345
+ }
346
+ /**
347
+ * Clear all swaps (for testing)
348
+ */
349
+ clear() {
350
+ this.cache.clear();
351
+ }
352
+ /**
353
+ * Purge stale entries (normally handled automatically by LRU cache)
354
+ */
355
+ purgeStale() {
356
+ this.cache.purgeStale();
357
+ }
358
+ };
359
+ var swapStore = new SwapStore();
360
+
361
+ // src/stores/webhook-store.ts
362
+ var import_crypto2 = require("crypto");
363
+ var WebhookStore = class {
364
+ registrations = /* @__PURE__ */ new Map();
365
+ maxSize;
366
+ constructor(maxSize) {
367
+ this.maxSize = maxSize ?? env.WEBHOOK_STORE_MAX_SIZE;
368
+ logger.info({ maxSize: this.maxSize }, "WebhookStore initialized");
369
+ }
370
+ /**
371
+ * Register a new webhook
372
+ *
373
+ * @returns The registration with secret (shown once only)
374
+ */
375
+ register(url, viewingPrivateKey, spendingPublicKey) {
376
+ if (this.registrations.size >= this.maxSize) {
377
+ throw new Error("WEBHOOK_STORE_FULL");
378
+ }
379
+ const id = (0, import_crypto2.randomBytes)(16).toString("hex");
380
+ const secret = (0, import_crypto2.randomBytes)(32).toString("hex");
381
+ const registration = {
382
+ id,
383
+ url,
384
+ viewingPrivateKey,
385
+ spendingPublicKey,
386
+ secret,
387
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
388
+ active: true
389
+ };
390
+ this.registrations.set(id, registration);
391
+ logger.info({ webhookId: id, url }, "Webhook registered");
392
+ return registration;
393
+ }
394
+ /**
395
+ * Unregister a webhook by ID
396
+ *
397
+ * @returns true if found and removed, false if not found
398
+ */
399
+ unregister(id) {
400
+ const existed = this.registrations.delete(id);
401
+ if (existed) {
402
+ logger.info({ webhookId: id }, "Webhook unregistered");
403
+ }
404
+ return existed;
405
+ }
406
+ /**
407
+ * Get a registration by ID
408
+ */
409
+ get(id) {
410
+ return this.registrations.get(id);
411
+ }
412
+ /**
413
+ * Get all active registrations
414
+ */
415
+ getAll() {
416
+ return Array.from(this.registrations.values()).filter((r) => r.active);
417
+ }
418
+ /**
419
+ * Get all registrations (including inactive) for listing
420
+ */
421
+ getAllForList() {
422
+ return Array.from(this.registrations.values()).map((r) => ({
423
+ id: r.id,
424
+ url: r.url,
425
+ active: r.active,
426
+ createdAt: r.createdAt
427
+ }));
428
+ }
429
+ /**
430
+ * Current store size
431
+ */
432
+ get size() {
433
+ return this.registrations.size;
434
+ }
435
+ /**
436
+ * Clear all registrations (for testing)
437
+ */
438
+ clear() {
439
+ this.registrations.clear();
440
+ }
441
+ };
442
+ var webhookStore = new WebhookStore();
443
+
444
+ // src/routes/proof.ts
445
+ var import_express = require("express");
65
446
  var import_sdk2 = require("@sip-protocol/sdk");
447
+ var import_utils = require("@noble/hashes/utils");
66
448
 
67
449
  // src/middleware/error-handler.ts
68
450
  var import_sdk = require("@sip-protocol/sdk");
451
+
452
+ // src/monitoring/sentry.ts
453
+ var Sentry = __toESM(require("@sentry/node"));
454
+ var isInitialized = false;
455
+ function initSentry() {
456
+ if (isInitialized) return;
457
+ if (!env.SENTRY_DSN) {
458
+ if (env.isProduction) {
459
+ console.warn("[Sentry] SENTRY_DSN not set - error monitoring disabled");
460
+ }
461
+ return;
462
+ }
463
+ Sentry.init({
464
+ dsn: env.SENTRY_DSN,
465
+ environment: env.NODE_ENV,
466
+ release: `sip-api@${process.env.npm_package_version || "0.1.0"}`,
467
+ // Performance monitoring
468
+ tracesSampleRate: env.isProduction ? 0.1 : 1,
469
+ // Filter out noisy errors
470
+ ignoreErrors: [
471
+ // Expected client errors
472
+ "Request validation failed",
473
+ "Unauthorized",
474
+ // Network issues
475
+ "ECONNRESET",
476
+ "EPIPE"
477
+ ],
478
+ // Add extra context
479
+ beforeSend(event) {
480
+ if (env.NODE_ENV === "test") {
481
+ return null;
482
+ }
483
+ const requestId = event.request?.headers?.["x-request-id"];
484
+ if (requestId && typeof requestId === "string") {
485
+ event.tags = { ...event.tags, requestId };
486
+ }
487
+ if (event.request?.headers) {
488
+ delete event.request.headers["authorization"];
489
+ delete event.request.headers["x-api-key"];
490
+ delete event.request.headers["cookie"];
491
+ }
492
+ return event;
493
+ },
494
+ // Capture breadcrumbs for debugging
495
+ beforeBreadcrumb(breadcrumb) {
496
+ if (breadcrumb.category === "http" && breadcrumb.data?.url?.includes("/health")) {
497
+ return null;
498
+ }
499
+ return breadcrumb;
500
+ }
501
+ });
502
+ isInitialized = true;
503
+ console.log("[Sentry] Initialized for environment:", env.NODE_ENV);
504
+ }
505
+ function isSentryEnabled() {
506
+ return isInitialized && !!env.SENTRY_DSN;
507
+ }
508
+ function captureException2(error, context) {
509
+ if (!isSentryEnabled()) return void 0;
510
+ return Sentry.captureException(error, {
511
+ extra: context
512
+ });
513
+ }
514
+ function setupSentryErrorHandler(app2) {
515
+ if (!isSentryEnabled()) return;
516
+ Sentry.setupExpressErrorHandler(app2);
517
+ }
518
+ async function flushSentry(timeout = 2e3) {
519
+ if (!isSentryEnabled()) return true;
520
+ return Sentry.close(timeout);
521
+ }
522
+
523
+ // src/monitoring/metrics.ts
524
+ var import_prom_client = require("prom-client");
525
+ var register = new import_prom_client.Registry();
526
+ (0, import_prom_client.collectDefaultMetrics)({
527
+ register,
528
+ prefix: "sip_api_"
529
+ });
530
+ var httpRequestsTotal = new import_prom_client.Counter({
531
+ name: "sip_api_http_requests_total",
532
+ help: "Total number of HTTP requests",
533
+ labelNames: ["method", "path", "status"],
534
+ registers: [register]
535
+ });
536
+ var httpRequestDuration = new import_prom_client.Histogram({
537
+ name: "sip_api_http_request_duration_seconds",
538
+ help: "Duration of HTTP requests in seconds",
539
+ labelNames: ["method", "path", "status"],
540
+ buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
541
+ registers: [register]
542
+ });
543
+ var stealthAddressGenerations = new import_prom_client.Counter({
544
+ name: "sip_stealth_address_generations_total",
545
+ help: "Total number of stealth address generations",
546
+ labelNames: ["chain"],
547
+ registers: [register]
548
+ });
549
+ var commitmentCreations = new import_prom_client.Counter({
550
+ name: "sip_commitment_creations_total",
551
+ help: "Total number of commitment creations",
552
+ registers: [register]
553
+ });
554
+ var proofGenerations = new import_prom_client.Counter({
555
+ name: "sip_proof_generations_total",
556
+ help: "Total number of proof generations",
557
+ labelNames: ["type"],
558
+ registers: [register]
559
+ });
560
+ var proofGenerationDuration = new import_prom_client.Histogram({
561
+ name: "sip_proof_generation_duration_seconds",
562
+ help: "Duration of proof generation in seconds",
563
+ labelNames: ["type"],
564
+ buckets: [0.1, 0.5, 1, 2.5, 5, 10, 30, 60],
565
+ registers: [register]
566
+ });
567
+ var activeConnections = new import_prom_client.Gauge({
568
+ name: "sip_api_active_connections",
569
+ help: "Number of active connections",
570
+ registers: [register]
571
+ });
572
+ var swapRequests = new import_prom_client.Counter({
573
+ name: "sip_swap_requests_total",
574
+ help: "Total number of swap requests",
575
+ labelNames: ["from_chain", "to_chain", "status"],
576
+ registers: [register]
577
+ });
578
+ var quoteRequests = new import_prom_client.Counter({
579
+ name: "sip_quote_requests_total",
580
+ help: "Total number of quote requests",
581
+ labelNames: ["from_chain", "to_chain"],
582
+ registers: [register]
583
+ });
584
+ function normalizePath(path) {
585
+ return path.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ":id").replace(/0x[0-9a-fA-F]{40,}/g, ":address").replace(/\/\d+/g, "/:id").replace(/\/+/g, "/");
586
+ }
587
+ function metricsMiddleware(req, res, next) {
588
+ if (req.path === "/metrics") {
589
+ return next();
590
+ }
591
+ const startTime2 = process.hrtime.bigint();
592
+ activeConnections.inc();
593
+ res.on("finish", () => {
594
+ const endTime = process.hrtime.bigint();
595
+ const durationSeconds = Number(endTime - startTime2) / 1e9;
596
+ const path = normalizePath(req.route?.path || req.path);
597
+ const labels = {
598
+ method: req.method,
599
+ path,
600
+ status: res.statusCode.toString()
601
+ };
602
+ httpRequestsTotal.inc(labels);
603
+ httpRequestDuration.observe(labels, durationSeconds);
604
+ activeConnections.dec();
605
+ });
606
+ next();
607
+ }
608
+
609
+ // src/middleware/error-handler.ts
69
610
  function errorHandler(err, req, res, next) {
70
- console.error("[API Error]", {
611
+ logger.error({
71
612
  path: req.path,
72
613
  method: req.method,
73
614
  error: err.message,
74
615
  stack: err.stack
75
- });
616
+ }, "API Error");
76
617
  if ((0, import_sdk.isSIPError)(err)) {
77
618
  const sipError = err;
619
+ const isValidationError = err instanceof import_sdk.ValidationError;
78
620
  return res.status(400).json({
79
621
  success: false,
80
622
  error: {
81
623
  code: sipError.code,
82
624
  message: sipError.message,
83
625
  details: {
84
- field: sipError.field,
85
- expected: sipError.expected
626
+ ...isValidationError && { field: err.field },
627
+ ...sipError.context
86
628
  }
87
629
  }
88
630
  });
89
631
  }
632
+ captureException2(err, {
633
+ path: req.path,
634
+ method: req.method
635
+ });
90
636
  res.status(500).json({
91
637
  success: false,
92
638
  error: {
93
639
  code: "INTERNAL_SERVER_ERROR",
94
640
  message: "An unexpected error occurred",
95
- details: process.env.NODE_ENV === "development" ? err.message : void 0
641
+ details: env.isDevelopment ? err.message : void 0
96
642
  }
97
643
  });
98
644
  }
@@ -108,6 +654,40 @@ function notFoundHandler(req, res) {
108
654
 
109
655
  // src/middleware/validation.ts
110
656
  var import_zod = require("zod");
657
+ var MAX_UINT256 = 2n ** 256n - 1n;
658
+ 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(
659
+ (v) => {
660
+ try {
661
+ const n = BigInt(v);
662
+ return n > 0n && n <= MAX_UINT256;
663
+ } catch {
664
+ return false;
665
+ }
666
+ },
667
+ { message: "Invalid amount: must be positive integer <= 2^256-1" }
668
+ );
669
+ 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(
670
+ (v) => {
671
+ try {
672
+ const n = BigInt(v);
673
+ return n >= 0n && n <= MAX_UINT256;
674
+ } catch {
675
+ return false;
676
+ }
677
+ },
678
+ { message: "Invalid amount: must be non-negative integer <= 2^256-1" }
679
+ );
680
+ function calculateMinAmount(input, slippageBps) {
681
+ if (slippageBps < 0 || slippageBps > 1e4) {
682
+ throw new Error("Invalid slippage: must be 0-10000 basis points");
683
+ }
684
+ const bps = BigInt(Math.floor(slippageBps));
685
+ const multiplier = 10000n - bps;
686
+ return input * multiplier / 10000n;
687
+ }
688
+ function percentToBps(percent) {
689
+ return Math.floor(percent * 100);
690
+ }
111
691
  function validateRequest(schema) {
112
692
  return async (req, res, next) => {
113
693
  try {
@@ -118,7 +698,9 @@ function validateRequest(schema) {
118
698
  req.query = await schema.query.parseAsync(req.query);
119
699
  }
120
700
  if (schema.params) {
121
- req.params = await schema.params.parseAsync(req.params);
701
+ req.params = await schema.params.parseAsync(
702
+ req.params
703
+ );
122
704
  }
123
705
  next();
124
706
  } catch (error) {
@@ -128,7 +710,8 @@ function validateRequest(schema) {
128
710
  error: {
129
711
  code: "VALIDATION_ERROR",
130
712
  message: "Invalid request data",
131
- details: error.errors
713
+ // Zod 4 renamed 'errors' to 'issues'
714
+ details: error.issues
132
715
  }
133
716
  });
134
717
  }
@@ -147,19 +730,19 @@ var schemas = {
147
730
  })
148
731
  }),
149
732
  createCommitment: import_zod.z.object({
150
- value: import_zod.z.string().regex(/^\d+$/),
151
- // bigint as string
733
+ value: amountSchema,
152
734
  blindingFactor: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/).optional()
153
735
  }),
154
736
  generateFundingProof: import_zod.z.object({
155
- balance: import_zod.z.string().regex(/^\d+$/),
156
- minRequired: import_zod.z.string().regex(/^\d+$/),
737
+ balance: amountSchema,
738
+ minRequired: minAmountSchema,
739
+ // Zero allowed: "prove I have >= 0" is valid
157
740
  balanceBlinding: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/)
158
741
  }),
159
742
  getQuote: import_zod.z.object({
160
743
  inputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
161
744
  inputToken: import_zod.z.string().min(1),
162
- inputAmount: import_zod.z.string().regex(/^\d+$/),
745
+ inputAmount: amountSchema,
163
746
  outputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
164
747
  outputToken: import_zod.z.string().min(1),
165
748
  slippageTolerance: import_zod.z.number().min(0).max(100).optional()
@@ -175,132 +758,476 @@ var schemas = {
175
758
  })
176
759
  };
177
760
 
178
- // src/routes/stealth.ts
179
- var router2 = (0, import_express2.Router)();
180
- router2.post(
181
- "/generate",
182
- validateRequest({ body: schemas.generateStealth }),
761
+ // src/middleware/rate-limit.ts
762
+ var import_express_rate_limit = __toESM(require("express-rate-limit"));
763
+ var import_rate_limit_redis = require("rate-limit-redis");
764
+ var import_ioredis = require("ioredis");
765
+ var WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000", 10);
766
+ var MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || "100", 10);
767
+ var SKIP_FAILED = process.env.RATE_LIMIT_SKIP_FAILED === "true";
768
+ var STORE_TYPE = process.env.RATE_LIMIT_STORE || "memory";
769
+ var REDIS_URL = process.env.REDIS_URL;
770
+ var redisClient = null;
771
+ var redisConnectionFailed = false;
772
+ function getRedisClient() {
773
+ if (redisConnectionFailed) {
774
+ return null;
775
+ }
776
+ if (!redisClient && REDIS_URL) {
777
+ redisClient = new import_ioredis.Redis(REDIS_URL, {
778
+ maxRetriesPerRequest: 3,
779
+ retryStrategy: (times) => {
780
+ if (times > 3) {
781
+ console.warn("[rate-limit] Redis connection failed, falling back to memory store");
782
+ redisConnectionFailed = true;
783
+ return null;
784
+ }
785
+ return Math.min(times * 100, 1e3);
786
+ },
787
+ lazyConnect: true
788
+ });
789
+ redisClient.on("error", (err) => {
790
+ console.warn("[rate-limit] Redis error:", err.message);
791
+ });
792
+ redisClient.on("connect", () => {
793
+ console.log("[rate-limit] Redis connected successfully");
794
+ });
795
+ }
796
+ return redisClient;
797
+ }
798
+ function getRedisStatus() {
799
+ const client = getRedisClient();
800
+ return {
801
+ storeType: client && !redisConnectionFailed ? "redis" : "memory",
802
+ configured: !!REDIS_URL,
803
+ connected: client?.status === "ready"
804
+ };
805
+ }
806
+ function createStore(prefix) {
807
+ if (STORE_TYPE === "redis" || REDIS_URL) {
808
+ const client = getRedisClient();
809
+ if (client && !redisConnectionFailed) {
810
+ const sendCommand = async (...args) => {
811
+ return client.call(args[0], ...args.slice(1));
812
+ };
813
+ return new import_rate_limit_redis.RedisStore({
814
+ sendCommand,
815
+ prefix: `rl:${prefix}:`
816
+ });
817
+ }
818
+ }
819
+ return void 0;
820
+ }
821
+ var rateLimiter = (0, import_express_rate_limit.default)({
822
+ windowMs: WINDOW_MS,
823
+ max: MAX_REQUESTS,
824
+ skipFailedRequests: SKIP_FAILED,
825
+ standardHeaders: true,
826
+ // Return rate limit info in `RateLimit-*` headers
827
+ legacyHeaders: false,
828
+ // Disable `X-RateLimit-*` headers
829
+ // Use Redis store if available, otherwise memory
830
+ store: createStore("api"),
831
+ handler: (_req, res) => {
832
+ res.status(429).json({
833
+ success: false,
834
+ error: {
835
+ code: "RATE_LIMIT_EXCEEDED",
836
+ message: "Too many requests, please try again later",
837
+ details: {
838
+ retryAfter: Math.ceil(WINDOW_MS / 1e3),
839
+ limit: MAX_REQUESTS,
840
+ windowMs: WINDOW_MS
841
+ }
842
+ }
843
+ });
844
+ },
845
+ skip: (req) => {
846
+ return req.path === "/api/v1/health" || req.path === "/";
847
+ }
848
+ });
849
+ var strictRateLimiter = (0, import_express_rate_limit.default)({
850
+ windowMs: 6e4,
851
+ // 1 minute
852
+ max: 10,
853
+ // 10 requests per minute
854
+ skipFailedRequests: false,
855
+ standardHeaders: true,
856
+ legacyHeaders: false,
857
+ // Use Redis store if available with different prefix
858
+ store: createStore("strict"),
859
+ handler: (_req, res) => {
860
+ res.status(429).json({
861
+ success: false,
862
+ error: {
863
+ code: "RATE_LIMIT_EXCEEDED",
864
+ message: "Rate limit exceeded for sensitive endpoint",
865
+ details: {
866
+ retryAfter: 60,
867
+ limit: 10,
868
+ windowMs: 6e4
869
+ }
870
+ }
871
+ });
872
+ }
873
+ });
874
+
875
+ // src/middleware/auth.ts
876
+ var import_crypto3 = require("crypto");
877
+ var API_KEYS = (process.env.API_KEYS || "").split(",").filter(Boolean);
878
+ var NODE_ENV = process.env.NODE_ENV || "development";
879
+ var AUTH_ENABLED = process.env.AUTH_ENABLED !== "false" && NODE_ENV === "production";
880
+ var SKIP_PATHS = (process.env.AUTH_SKIP_PATHS || "/health,/,/webhooks/internal/helius").split(",").map((p) => p.trim());
881
+ function safeCompare(a, b) {
882
+ if (a.length !== b.length) {
883
+ return false;
884
+ }
885
+ try {
886
+ return (0, import_crypto3.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
887
+ } catch {
888
+ return false;
889
+ }
890
+ }
891
+ function isValidApiKey(key) {
892
+ return API_KEYS.some((validKey) => safeCompare(key, validKey));
893
+ }
894
+ function extractApiKey(req) {
895
+ const apiKeyHeader = req.headers["x-api-key"];
896
+ if (typeof apiKeyHeader === "string" && apiKeyHeader) {
897
+ return apiKeyHeader;
898
+ }
899
+ const authHeader = req.headers.authorization;
900
+ if (authHeader?.startsWith("Bearer ")) {
901
+ return authHeader.slice(7);
902
+ }
903
+ return null;
904
+ }
905
+ function authenticate(req, res, next) {
906
+ if (!AUTH_ENABLED) {
907
+ return next();
908
+ }
909
+ const path = req.path.replace("/api/v1", "");
910
+ if (SKIP_PATHS.some((skipPath) => path === skipPath || path.startsWith(skipPath + "/"))) {
911
+ return next();
912
+ }
913
+ if (API_KEYS.length === 0) {
914
+ console.warn("[Auth] No API keys configured. Set API_KEYS environment variable.");
915
+ return res.status(500).json({
916
+ success: false,
917
+ error: {
918
+ code: "AUTH_NOT_CONFIGURED",
919
+ message: "Authentication is enabled but no API keys are configured"
920
+ }
921
+ });
922
+ }
923
+ const apiKey = extractApiKey(req);
924
+ if (!apiKey) {
925
+ return res.status(401).json({
926
+ success: false,
927
+ error: {
928
+ code: "UNAUTHORIZED",
929
+ message: "API key required. Provide via X-API-Key header or Authorization: Bearer <key>"
930
+ }
931
+ });
932
+ }
933
+ if (!isValidApiKey(apiKey)) {
934
+ return res.status(401).json({
935
+ success: false,
936
+ error: {
937
+ code: "INVALID_API_KEY",
938
+ message: "Invalid API key"
939
+ }
940
+ });
941
+ }
942
+ next();
943
+ }
944
+ function isAuthEnabled() {
945
+ return AUTH_ENABLED;
946
+ }
947
+
948
+ // src/middleware/cors.ts
949
+ var import_cors = __toESM(require("cors"));
950
+ var NODE_ENV2 = process.env.NODE_ENV || "development";
951
+ var CORS_ORIGINS = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean) || [];
952
+ var CORS_CREDENTIALS = process.env.CORS_CREDENTIALS !== "false";
953
+ var CORS_MAX_AGE = parseInt(process.env.CORS_MAX_AGE || "86400", 10);
954
+ var DEV_ORIGINS = [
955
+ "http://localhost:3000",
956
+ "http://localhost:3001",
957
+ "http://localhost:4000",
958
+ "http://localhost:5173",
959
+ // Vite
960
+ "http://127.0.0.1:3000",
961
+ "http://127.0.0.1:3001",
962
+ "http://127.0.0.1:4000",
963
+ "http://127.0.0.1:5173"
964
+ ];
965
+ function getAllowedOrigins() {
966
+ if (CORS_ORIGINS.length > 0) {
967
+ return CORS_ORIGINS;
968
+ }
969
+ if (NODE_ENV2 === "development" || NODE_ENV2 === "test") {
970
+ return DEV_ORIGINS;
971
+ }
972
+ return [];
973
+ }
974
+ function isOriginAllowed(origin) {
975
+ if (!origin) {
976
+ return true;
977
+ }
978
+ const allowedOrigins = getAllowedOrigins();
979
+ if (allowedOrigins.includes(origin)) {
980
+ return true;
981
+ }
982
+ let originUrl;
983
+ try {
984
+ originUrl = new URL(origin);
985
+ } catch {
986
+ console.warn(`[CORS] Rejected malformed origin: ${origin}`);
987
+ return false;
988
+ }
989
+ for (const allowed of allowedOrigins) {
990
+ if (allowed.startsWith("*.")) {
991
+ const baseDomain = allowed.slice(2);
992
+ const originHost = originUrl.host;
993
+ if (NODE_ENV2 === "production" && originUrl.protocol !== "https:") {
994
+ console.warn(`[CORS] Rejected non-HTTPS origin in production: ${origin}`);
995
+ continue;
996
+ }
997
+ if (originHost === baseDomain || originHost.endsWith("." + baseDomain)) {
998
+ return true;
999
+ }
1000
+ }
1001
+ }
1002
+ return false;
1003
+ }
1004
+ var corsOptionsDelegate = (req, callback) => {
1005
+ const origin = req.headers.origin;
1006
+ const allowed = isOriginAllowed(origin);
1007
+ const options = {
1008
+ origin: allowed ? origin : false,
1009
+ credentials: CORS_CREDENTIALS,
1010
+ maxAge: CORS_MAX_AGE,
1011
+ methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
1012
+ allowedHeaders: [
1013
+ "Content-Type",
1014
+ "Authorization",
1015
+ "X-API-Key",
1016
+ "X-Request-ID",
1017
+ "X-Forwarded-For"
1018
+ ],
1019
+ exposedHeaders: [
1020
+ "RateLimit-Limit",
1021
+ "RateLimit-Remaining",
1022
+ "RateLimit-Reset",
1023
+ "X-Request-ID"
1024
+ ]
1025
+ };
1026
+ if (!allowed && origin) {
1027
+ console.warn(`[CORS] Blocked request from origin: ${origin}`);
1028
+ }
1029
+ callback(null, options);
1030
+ };
1031
+ var secureCors = (0, import_cors.default)(corsOptionsDelegate);
1032
+ function getCorsConfig() {
1033
+ return {
1034
+ origins: getAllowedOrigins(),
1035
+ credentials: CORS_CREDENTIALS,
1036
+ maxAge: CORS_MAX_AGE,
1037
+ environment: NODE_ENV2
1038
+ };
1039
+ }
1040
+
1041
+ // src/middleware/request-id.ts
1042
+ var import_crypto4 = __toESM(require("crypto"));
1043
+ var requestIdMiddleware = (req, res, next) => {
1044
+ const clientId = req.headers["x-request-id"];
1045
+ const requestId = typeof clientId === "string" && clientId.length > 0 ? clientId : import_crypto4.default.randomUUID();
1046
+ req.requestId = requestId;
1047
+ res.setHeader("X-Request-ID", requestId);
1048
+ next();
1049
+ };
1050
+
1051
+ // src/routes/proof.ts
1052
+ var router = (0, import_express.Router)();
1053
+ var proofProvider = new import_sdk2.MockProofProvider();
1054
+ var proofProviderReady = false;
1055
+ var proofInitError = null;
1056
+ var MAX_INIT_RETRIES = 3;
1057
+ var RETRY_DELAY_MS = 2e3;
1058
+ async function initializeProofProvider() {
1059
+ for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
1060
+ try {
1061
+ await proofProvider.initialize();
1062
+ proofProviderReady = true;
1063
+ proofInitError = null;
1064
+ logger.info({ attempt }, "Proof provider initialized successfully");
1065
+ return;
1066
+ } catch (err) {
1067
+ proofInitError = err instanceof Error ? err : new Error(String(err));
1068
+ logger.warn(
1069
+ { attempt, maxRetries: MAX_INIT_RETRIES, error: proofInitError.message },
1070
+ "Proof provider initialization failed, retrying..."
1071
+ );
1072
+ if (attempt < MAX_INIT_RETRIES) {
1073
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt));
1074
+ }
1075
+ }
1076
+ }
1077
+ logger.error(
1078
+ { error: proofInitError?.message },
1079
+ "Proof provider initialization failed after all retries"
1080
+ );
1081
+ }
1082
+ initializeProofProvider();
1083
+ function isProofProviderReady() {
1084
+ return proofProviderReady;
1085
+ }
1086
+ function getProofInitError() {
1087
+ return proofInitError;
1088
+ }
1089
+ router.post(
1090
+ "/funding",
1091
+ validateRequest({ body: schemas.generateFundingProof }),
183
1092
  async (req, res) => {
184
- const { chain, recipientMetaAddress } = req.body;
185
- const result = (0, import_sdk2.generateStealthAddress)(recipientMetaAddress);
1093
+ if (!proofProviderReady) {
1094
+ const errorMsg = proofInitError?.message || "Proof provider is initializing";
1095
+ logger.warn({ error: errorMsg }, "Proof request rejected - provider not ready");
1096
+ return res.status(503).json({
1097
+ success: false,
1098
+ error: {
1099
+ code: "PROOF_PROVIDER_NOT_READY",
1100
+ message: "Proof generation service is not ready",
1101
+ details: { reason: errorMsg }
1102
+ }
1103
+ });
1104
+ }
1105
+ const { balance, minRequired, balanceBlinding } = req.body;
1106
+ const balanceBigInt = BigInt(balance);
1107
+ const minRequiredBigInt = BigInt(minRequired);
1108
+ const balanceBlindingBytes = (0, import_utils.hexToBytes)(balanceBlinding.replace(/^0x/, ""));
1109
+ const result = await proofProvider.generateFundingProof({
1110
+ balance: balanceBigInt,
1111
+ minimumRequired: minRequiredBigInt,
1112
+ blindingFactor: balanceBlindingBytes,
1113
+ assetId: "SOL",
1114
+ // Default asset
1115
+ userAddress: "0x0000000000000000000000000000000000000000",
1116
+ ownershipSignature: new Uint8Array(64)
1117
+ });
186
1118
  const response = {
187
1119
  success: true,
188
1120
  data: {
189
- stealthAddress: {
190
- address: result.stealthAddress.address,
191
- ephemeralPublicKey: result.stealthAddress.ephemeralPublicKey,
192
- viewTag: result.stealthAddress.viewTag
193
- }
1121
+ proof: result.proof.proof,
1122
+ publicInputs: result.proof.publicInputs
194
1123
  }
195
1124
  };
196
1125
  res.json(response);
197
1126
  }
198
1127
  );
199
- var stealth_default = router2;
1128
+ var proof_default = router;
200
1129
 
201
- // src/routes/commitment.ts
1130
+ // src/routes/health.ts
1131
+ var router2 = (0, import_express2.Router)();
1132
+ var startTime = Date.now();
1133
+ router2.get("/", (req, res) => {
1134
+ const shuttingDown = isServerShuttingDown();
1135
+ const status = shuttingDown ? "shutting_down" : "healthy";
1136
+ const statusCode = shuttingDown ? 503 : 200;
1137
+ const cacheMetrics = swapStore.getMetrics();
1138
+ const proofReady = isProofProviderReady();
1139
+ const proofError = getProofInitError();
1140
+ const redisStatus = getRedisStatus();
1141
+ const response = {
1142
+ success: !shuttingDown,
1143
+ data: {
1144
+ status,
1145
+ version: process.env.npm_package_version || "0.1.0",
1146
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1147
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
1148
+ services: {
1149
+ proofProvider: {
1150
+ ready: proofReady,
1151
+ error: proofError?.message || null
1152
+ },
1153
+ rateLimiter: {
1154
+ store: redisStatus.storeType,
1155
+ redisConfigured: redisStatus.configured,
1156
+ redisConnected: redisStatus.connected
1157
+ }
1158
+ },
1159
+ cache: {
1160
+ swaps: {
1161
+ size: cacheMetrics.size,
1162
+ maxSize: cacheMetrics.maxSize,
1163
+ utilizationPercent: cacheMetrics.utilizationPercent
1164
+ }
1165
+ }
1166
+ }
1167
+ };
1168
+ res.status(statusCode).json(response);
1169
+ });
1170
+ var health_default = router2;
1171
+
1172
+ // src/routes/stealth.ts
202
1173
  var import_express3 = require("express");
203
1174
  var import_sdk3 = require("@sip-protocol/sdk");
204
-
205
- // ../../../../node_modules/@noble/hashes/esm/utils.js
206
- var hasHexBuiltin = /* @__PURE__ */ (() => (
207
- // @ts-ignore
208
- typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function"
209
- ))();
210
- var asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
211
- function asciiToBase16(ch) {
212
- if (ch >= asciis._0 && ch <= asciis._9)
213
- return ch - asciis._0;
214
- if (ch >= asciis.A && ch <= asciis.F)
215
- return ch - (asciis.A - 10);
216
- if (ch >= asciis.a && ch <= asciis.f)
217
- return ch - (asciis.a - 10);
218
- return;
219
- }
220
- function hexToBytes(hex) {
221
- if (typeof hex !== "string")
222
- throw new Error("hex string expected, got " + typeof hex);
223
- if (hasHexBuiltin)
224
- return Uint8Array.fromHex(hex);
225
- const hl = hex.length;
226
- const al = hl / 2;
227
- if (hl % 2)
228
- throw new Error("hex string expected, got unpadded hex of length " + hl);
229
- const array = new Uint8Array(al);
230
- for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
231
- const n1 = asciiToBase16(hex.charCodeAt(hi));
232
- const n2 = asciiToBase16(hex.charCodeAt(hi + 1));
233
- if (n1 === void 0 || n2 === void 0) {
234
- const char = hex[hi] + hex[hi + 1];
235
- throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi);
236
- }
237
- array[ai] = n1 * 16 + n2;
238
- }
239
- return array;
240
- }
241
-
242
- // src/routes/commitment.ts
243
1175
  var router3 = (0, import_express3.Router)();
244
1176
  router3.post(
245
- "/create",
246
- validateRequest({ body: schemas.createCommitment }),
1177
+ "/generate",
1178
+ validateRequest({ body: schemas.generateStealth }),
247
1179
  async (req, res) => {
248
- const { value, blindingFactor } = req.body;
249
- const valueBigInt = BigInt(value);
250
- const blindingBytes = blindingFactor ? hexToBytes(blindingFactor) : void 0;
251
- const result = (0, import_sdk3.commit)(valueBigInt, blindingBytes);
1180
+ const { chain, recipientMetaAddress } = req.body;
1181
+ const result = (0, import_sdk3.generateStealthAddress)(recipientMetaAddress);
252
1182
  const response = {
253
1183
  success: true,
254
1184
  data: {
255
- commitment: result.commitment,
256
- blindingFactor: result.blinding
1185
+ stealthAddress: {
1186
+ address: result.stealthAddress.address,
1187
+ ephemeralPublicKey: result.stealthAddress.ephemeralPublicKey,
1188
+ viewTag: result.stealthAddress.viewTag
1189
+ }
257
1190
  }
258
1191
  };
259
1192
  res.json(response);
260
1193
  }
261
1194
  );
262
- var commitment_default = router3;
1195
+ var stealth_default = router3;
263
1196
 
264
- // src/routes/proof.ts
1197
+ // src/routes/commitment.ts
265
1198
  var import_express4 = require("express");
266
1199
  var import_sdk4 = require("@sip-protocol/sdk");
1200
+ var import_utils2 = require("@noble/hashes/utils");
267
1201
  var router4 = (0, import_express4.Router)();
268
- var proofProvider = new import_sdk4.MockProofProvider();
269
1202
  router4.post(
270
- "/funding",
271
- validateRequest({ body: schemas.generateFundingProof }),
1203
+ "/create",
1204
+ validateRequest({ body: schemas.createCommitment }),
272
1205
  async (req, res) => {
273
- const { balance, minRequired, balanceBlinding } = req.body;
274
- const balanceBigInt = BigInt(balance);
275
- const minRequiredBigInt = BigInt(minRequired);
276
- const balanceBlindingBytes = hexToBytes(balanceBlinding);
277
- const result = await proofProvider.generateFundingProof({
278
- balance: balanceBigInt,
279
- minimumRequired: minRequiredBigInt,
280
- blindingFactor: balanceBlindingBytes,
281
- assetId: "SOL",
282
- // Default asset
283
- userAddress: "0x0000000000000000000000000000000000000000",
284
- ownershipSignature: new Uint8Array(64)
285
- });
1206
+ const { value, blindingFactor } = req.body;
1207
+ const valueBigInt = BigInt(value);
1208
+ const blindingBytes = blindingFactor ? (0, import_utils2.hexToBytes)(blindingFactor.replace(/^0x/, "")) : void 0;
1209
+ const result = (0, import_sdk4.commit)(valueBigInt, blindingBytes);
286
1210
  const response = {
287
1211
  success: true,
288
1212
  data: {
289
- proof: result.proof.proof,
290
- publicInputs: result.proof.publicInputs
1213
+ commitment: result.commitment,
1214
+ blindingFactor: result.blinding
291
1215
  }
292
1216
  };
293
1217
  res.json(response);
294
1218
  }
295
1219
  );
296
- var proof_default = router4;
1220
+ var commitment_default = router4;
297
1221
 
298
1222
  // src/routes/swap.ts
299
1223
  var import_express5 = require("express");
300
1224
  var import_sdk5 = require("@sip-protocol/sdk");
301
1225
  var router5 = (0, import_express5.Router)();
302
1226
  var sip = new import_sdk5.SIP({ network: "testnet" });
303
- var swaps = /* @__PURE__ */ new Map();
1227
+ var MOCK_MODE_ENABLED = env.NODE_ENV !== "production";
1228
+ if (!MOCK_MODE_ENABLED) {
1229
+ logger.warn("Production mode: Mock quotes and swaps are DISABLED");
1230
+ }
304
1231
  router5.post(
305
1232
  "/",
306
1233
  validateRequest({ body: schemas.getQuote }),
@@ -313,37 +1240,66 @@ router5.post(
313
1240
  outputToken,
314
1241
  slippageTolerance
315
1242
  } = req.body;
1243
+ if (!MOCK_MODE_ENABLED) {
1244
+ return res.status(503).json({
1245
+ success: false,
1246
+ error: {
1247
+ code: "QUOTE_SERVICE_UNAVAILABLE",
1248
+ message: "Real quote aggregator not configured. This API is not ready for production use.",
1249
+ details: {
1250
+ hint: "Configure QUOTE_AGGREGATOR_URL or use development mode"
1251
+ }
1252
+ }
1253
+ });
1254
+ }
1255
+ if (!(0, import_sdk5.isKnownToken)(inputToken, inputChain)) {
1256
+ return res.status(400).json({
1257
+ success: false,
1258
+ error: {
1259
+ code: "UNKNOWN_TOKEN",
1260
+ message: `Unknown input token: ${inputToken} on ${inputChain}`
1261
+ }
1262
+ });
1263
+ }
1264
+ if (!(0, import_sdk5.isKnownToken)(outputToken, outputChain)) {
1265
+ return res.status(400).json({
1266
+ success: false,
1267
+ error: {
1268
+ code: "UNKNOWN_TOKEN",
1269
+ message: `Unknown output token: ${outputToken} on ${outputChain}`
1270
+ }
1271
+ });
1272
+ }
1273
+ const inputAsset = (0, import_sdk5.getAsset)(inputToken, inputChain);
1274
+ const outputAsset = (0, import_sdk5.getAsset)(outputToken, outputChain);
1275
+ const inputAmountBigInt = BigInt(inputAmount);
1276
+ const slippagePercent = slippageTolerance ?? 1;
1277
+ const slippageBps = percentToBps(slippagePercent);
316
1278
  const intent = await sip.createIntent({
317
1279
  input: {
318
- asset: {
319
- chain: inputChain,
320
- address: null,
321
- // Native token
322
- symbol: inputToken,
323
- decimals: 9
324
- },
325
- amount: BigInt(inputAmount)
1280
+ asset: inputAsset,
1281
+ amount: inputAmountBigInt
326
1282
  },
327
1283
  output: {
328
- asset: {
329
- chain: outputChain,
330
- address: null,
331
- // Native token
332
- symbol: outputToken,
333
- decimals: 9
334
- },
335
- minAmount: BigInt(inputAmount) * 95n / 100n,
336
- // 5% slippage
337
- maxSlippage: (slippageTolerance || 1) / 100
1284
+ asset: outputAsset,
1285
+ minAmount: calculateMinAmount(inputAmountBigInt, slippageBps),
1286
+ maxSlippage: slippagePercent / 100
338
1287
  },
339
1288
  privacy: import_sdk5.PrivacyLevel.TRANSPARENT
340
1289
  // Default to transparent for quote
341
1290
  });
1291
+ logger.warn({
1292
+ inputChain,
1293
+ inputToken,
1294
+ outputChain,
1295
+ outputToken,
1296
+ inputAmount
1297
+ }, "Returning MOCK quote - not for production use");
342
1298
  const mockQuote = {
343
1299
  quoteId: `quote-${Date.now()}`,
344
1300
  inputAmount,
1301
+ // Mock: 5% fee (clearly fake rate)
345
1302
  outputAmount: (BigInt(inputAmount) * 95n / 100n).toString(),
346
- // Mock 5% fee
347
1303
  rate: "0.95",
348
1304
  estimatedTime: 30,
349
1305
  fees: {
@@ -363,7 +1319,10 @@ router5.post(
363
1319
  };
364
1320
  const response = {
365
1321
  success: true,
366
- data: mockQuote
1322
+ data: {
1323
+ ...mockQuote,
1324
+ _warning: "MOCK_DATA: This quote uses simulated pricing. Do not use for real transactions."
1325
+ }
367
1326
  };
368
1327
  res.json(response);
369
1328
  }
@@ -372,23 +1331,45 @@ router5.post(
372
1331
  "/swap",
373
1332
  validateRequest({ body: schemas.executeSwap }),
374
1333
  async (req, res) => {
375
- const { intentId, quoteId, privacy, viewingKey } = req.body;
1334
+ const { intentId, quoteId, inputAmount } = req.body;
1335
+ if (!MOCK_MODE_ENABLED) {
1336
+ return res.status(503).json({
1337
+ success: false,
1338
+ error: {
1339
+ code: "SWAP_SERVICE_UNAVAILABLE",
1340
+ message: "Real swap executor not configured. This API is not ready for production use.",
1341
+ details: {
1342
+ hint: "Configure SWAP_EXECUTOR_URL or use development mode"
1343
+ }
1344
+ }
1345
+ });
1346
+ }
376
1347
  const swapId = `swap-${Date.now()}`;
1348
+ const actualInputAmount = inputAmount || "0";
1349
+ if (!inputAmount) {
1350
+ logger.warn({ swapId, quoteId, intentId }, "Swap created without inputAmount - using 0");
1351
+ }
377
1352
  const swap = {
378
1353
  id: swapId,
379
1354
  status: "pending",
380
- inputAmount: "1000000000",
381
- // Mock value
1355
+ inputAmount: actualInputAmount,
382
1356
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
383
1357
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
384
1358
  };
385
- swaps.set(swapId, swap);
1359
+ swapStore.set(swapId, swap);
1360
+ logger.warn({
1361
+ swapId,
1362
+ quoteId,
1363
+ intentId,
1364
+ inputAmount: actualInputAmount
1365
+ }, "Creating MOCK swap - not for production use");
386
1366
  const response = {
387
1367
  success: true,
388
1368
  data: {
389
1369
  swapId,
390
1370
  status: "pending",
391
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1371
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1372
+ _warning: "MOCK_DATA: This swap is simulated. No real transaction will be executed."
392
1373
  }
393
1374
  };
394
1375
  res.json(response);
@@ -399,13 +1380,14 @@ router5.get(
399
1380
  validateRequest({ params: schemas.swapStatus }),
400
1381
  async (req, res) => {
401
1382
  const { id } = req.params;
402
- const swap = swaps.get(id);
1383
+ const swapId = Array.isArray(id) ? id[0] : id;
1384
+ const swap = swapStore.get(swapId);
403
1385
  if (!swap) {
404
1386
  return res.status(404).json({
405
1387
  success: false,
406
1388
  error: {
407
1389
  code: "SWAP_NOT_FOUND",
408
- message: `Swap ${id} not found`
1390
+ message: `Swap ${swapId} not found`
409
1391
  }
410
1392
  });
411
1393
  }
@@ -418,32 +1400,344 @@ router5.get(
418
1400
  );
419
1401
  var swap_default = router5;
420
1402
 
421
- // src/routes/index.ts
1403
+ // src/routes/webhook.ts
1404
+ var import_express6 = require("express");
1405
+ var import_zod2 = require("zod");
1406
+
1407
+ // src/services/helius-listener.ts
1408
+ var import_sdk6 = require("@sip-protocol/sdk");
1409
+
1410
+ // src/services/webhook-delivery.ts
1411
+ var import_hmac = require("@noble/hashes/hmac");
1412
+ var import_sha256 = require("@noble/hashes/sha256");
1413
+ var import_utils3 = require("@noble/hashes/utils");
1414
+ function computeHmacSignature(secret, body) {
1415
+ const encoder = new TextEncoder();
1416
+ const sig = (0, import_utils3.bytesToHex)((0, import_hmac.hmac)(import_sha256.sha256, encoder.encode(secret), encoder.encode(body)));
1417
+ return `sha256=${sig}`;
1418
+ }
1419
+ function sleep(ms) {
1420
+ return new Promise((resolve) => setTimeout(resolve, ms));
1421
+ }
1422
+ var WebhookDeliveryService = class {
1423
+ maxRetries;
1424
+ pendingDeliveries = /* @__PURE__ */ new Set();
1425
+ constructor(maxRetries) {
1426
+ this.maxRetries = maxRetries ?? env.WEBHOOK_DELIVERY_MAX_RETRIES;
1427
+ }
1428
+ /**
1429
+ * Build the delivery payload from a scan result
1430
+ */
1431
+ buildPayload(registration, payment) {
1432
+ return {
1433
+ event: "payment.received",
1434
+ webhookId: registration.id,
1435
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1436
+ data: {
1437
+ txSignature: payment.txSignature,
1438
+ stealthAddress: payment.stealthAddress,
1439
+ ephemeralPublicKey: payment.ephemeralPublicKey,
1440
+ amount: payment.amount.toString(),
1441
+ mint: payment.mint,
1442
+ tokenSymbol: payment.tokenSymbol,
1443
+ slot: payment.slot,
1444
+ blockTime: payment.timestamp
1445
+ }
1446
+ };
1447
+ }
1448
+ /**
1449
+ * Deliver a payment notification to a registered webhook
1450
+ *
1451
+ * Retries with exponential backoff on failure.
1452
+ */
1453
+ async deliver(registration, payment) {
1454
+ const payload = this.buildPayload(registration, payment);
1455
+ const body = JSON.stringify(payload);
1456
+ const signature = computeHmacSignature(registration.secret, body);
1457
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
1458
+ try {
1459
+ const response = await fetch(registration.url, {
1460
+ method: "POST",
1461
+ headers: {
1462
+ "Content-Type": "application/json",
1463
+ "X-SIP-Signature": signature,
1464
+ "X-SIP-Webhook-Id": registration.id
1465
+ },
1466
+ body,
1467
+ signal: AbortSignal.timeout(1e4)
1468
+ });
1469
+ if (response.ok) {
1470
+ logger.info(
1471
+ { webhookId: registration.id, attempt, status: response.status },
1472
+ "Webhook delivered"
1473
+ );
1474
+ return true;
1475
+ }
1476
+ logger.warn(
1477
+ { webhookId: registration.id, attempt, status: response.status },
1478
+ "Webhook delivery failed (non-2xx)"
1479
+ );
1480
+ } catch (error) {
1481
+ logger.warn(
1482
+ { webhookId: registration.id, attempt, error: error.message },
1483
+ "Webhook delivery error"
1484
+ );
1485
+ }
1486
+ if (attempt < this.maxRetries) {
1487
+ const backoffMs = Math.pow(4, attempt) * 1e3;
1488
+ await sleep(backoffMs);
1489
+ }
1490
+ }
1491
+ logger.error(
1492
+ { webhookId: registration.id, maxRetries: this.maxRetries },
1493
+ "Webhook delivery exhausted all retries"
1494
+ );
1495
+ return false;
1496
+ }
1497
+ /**
1498
+ * Queue a delivery (fire-and-forget with tracking)
1499
+ */
1500
+ queueDelivery(registration, payment) {
1501
+ const promise = this.deliver(registration, payment).then(() => {
1502
+ }).catch((err) => {
1503
+ logger.error({ webhookId: registration.id, err }, "Unhandled webhook delivery error");
1504
+ }).finally(() => {
1505
+ this.pendingDeliveries.delete(promise);
1506
+ });
1507
+ this.pendingDeliveries.add(promise);
1508
+ }
1509
+ /**
1510
+ * Wait for all pending deliveries to complete (graceful shutdown)
1511
+ */
1512
+ async drainPending() {
1513
+ if (this.pendingDeliveries.size > 0) {
1514
+ logger.info({ pending: this.pendingDeliveries.size }, "Draining pending webhook deliveries");
1515
+ await Promise.allSettled(this.pendingDeliveries);
1516
+ logger.info("All webhook deliveries drained");
1517
+ }
1518
+ }
1519
+ };
1520
+ var webhookDeliveryService = new WebhookDeliveryService();
1521
+
1522
+ // src/services/helius-listener.ts
1523
+ var HeliusListenerService = class {
1524
+ /**
1525
+ * Process an incoming Helius webhook payload
1526
+ *
1527
+ * 1. Verify Helius signature (if configured)
1528
+ * 2. For each active registration, check if payment is for them
1529
+ * 3. On match, queue delivery to the agent's URL
1530
+ *
1531
+ * @returns Number of matched deliveries queued
1532
+ */
1533
+ async processIncoming(payload, headers) {
1534
+ if (env.HELIUS_WEBHOOK_SECRET && headers.rawBody) {
1535
+ const valid = (0, import_sdk6.verifyWebhookSignature)(
1536
+ headers.rawBody,
1537
+ headers.signature,
1538
+ env.HELIUS_WEBHOOK_SECRET
1539
+ );
1540
+ if (!valid) {
1541
+ logger.warn("Helius webhook signature verification failed");
1542
+ throw new Error("HELIUS_SIGNATURE_INVALID");
1543
+ }
1544
+ }
1545
+ const transactions = Array.isArray(payload) ? payload : [payload];
1546
+ const registrations = webhookStore.getAll();
1547
+ if (registrations.length === 0) {
1548
+ logger.debug("No active webhook registrations, skipping processing");
1549
+ return 0;
1550
+ }
1551
+ let matchCount = 0;
1552
+ for (const tx of transactions) {
1553
+ for (const registration of registrations) {
1554
+ try {
1555
+ const payment = await (0, import_sdk6.processWebhookTransaction)(
1556
+ tx,
1557
+ registration.viewingPrivateKey,
1558
+ registration.spendingPublicKey
1559
+ );
1560
+ if (payment) {
1561
+ matchCount++;
1562
+ webhookDeliveryService.queueDelivery(registration, payment);
1563
+ logger.info(
1564
+ {
1565
+ webhookId: registration.id,
1566
+ txSignature: payment.txSignature
1567
+ },
1568
+ "Payment matched, delivery queued"
1569
+ );
1570
+ }
1571
+ } catch (error) {
1572
+ logger.warn(
1573
+ {
1574
+ webhookId: registration.id,
1575
+ error: error.message
1576
+ },
1577
+ "Error checking transaction against registration"
1578
+ );
1579
+ }
1580
+ }
1581
+ }
1582
+ logger.info(
1583
+ {
1584
+ transactions: transactions.length,
1585
+ registrations: registrations.length,
1586
+ matches: matchCount
1587
+ },
1588
+ "Helius webhook processed"
1589
+ );
1590
+ return matchCount;
1591
+ }
1592
+ };
1593
+ var heliusListenerService = new HeliusListenerService();
1594
+
1595
+ // src/routes/webhook.ts
422
1596
  var router6 = (0, import_express6.Router)();
423
- router6.use("/health", health_default);
424
- router6.use("/stealth", stealth_default);
425
- router6.use("/commitment", commitment_default);
426
- router6.use("/proof", proof_default);
427
- router6.use("/quote", swap_default);
428
- router6.use("/swap", swap_default);
429
- var routes_default = router6;
1597
+ var webhookSchemas = {
1598
+ register: import_zod2.z.object({
1599
+ url: import_zod2.z.string().url("Must be a valid URL"),
1600
+ viewingPrivateKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex"),
1601
+ spendingPublicKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex")
1602
+ }),
1603
+ unregister: import_zod2.z.object({
1604
+ id: import_zod2.z.string().min(1)
1605
+ })
1606
+ };
1607
+ router6.post(
1608
+ "/register",
1609
+ validateRequest({ body: webhookSchemas.register }),
1610
+ (req, res) => {
1611
+ const { url, viewingPrivateKey, spendingPublicKey } = req.body;
1612
+ try {
1613
+ const registration = webhookStore.register(url, viewingPrivateKey, spendingPublicKey);
1614
+ const response = {
1615
+ success: true,
1616
+ data: {
1617
+ id: registration.id,
1618
+ url: registration.url,
1619
+ secret: registration.secret,
1620
+ createdAt: registration.createdAt
1621
+ }
1622
+ };
1623
+ res.status(201).json(response);
1624
+ } catch (error) {
1625
+ if (error.message === "WEBHOOK_STORE_FULL") {
1626
+ res.status(503).json({
1627
+ success: false,
1628
+ error: {
1629
+ code: "WEBHOOK_STORE_FULL",
1630
+ message: "Maximum webhook registrations reached"
1631
+ }
1632
+ });
1633
+ return;
1634
+ }
1635
+ throw error;
1636
+ }
1637
+ }
1638
+ );
1639
+ router6.delete(
1640
+ "/:id",
1641
+ validateRequest({ params: webhookSchemas.unregister }),
1642
+ (req, res) => {
1643
+ const removed = webhookStore.unregister(req.params.id);
1644
+ if (!removed) {
1645
+ res.status(404).json({
1646
+ success: false,
1647
+ error: {
1648
+ code: "WEBHOOK_NOT_FOUND",
1649
+ message: "Webhook not found"
1650
+ }
1651
+ });
1652
+ return;
1653
+ }
1654
+ res.status(204).send();
1655
+ }
1656
+ );
1657
+ router6.get("/", (_req, res) => {
1658
+ const webhooks = webhookStore.getAllForList();
1659
+ const response = {
1660
+ success: true,
1661
+ data: { webhooks }
1662
+ };
1663
+ res.json(response);
1664
+ });
1665
+ router6.post("/internal/helius", async (req, res) => {
1666
+ const headers = {
1667
+ signature: req.headers["x-helius-signature"],
1668
+ rawBody: typeof req.body === "string" ? req.body : JSON.stringify(req.body)
1669
+ };
1670
+ try {
1671
+ const matchCount = await heliusListenerService.processIncoming(req.body, headers);
1672
+ res.status(200).json({ success: true, matched: matchCount });
1673
+ } catch (error) {
1674
+ if (error.message === "HELIUS_SIGNATURE_INVALID") {
1675
+ res.status(401).json({
1676
+ success: false,
1677
+ error: {
1678
+ code: "HELIUS_SIGNATURE_INVALID",
1679
+ message: "Invalid Helius webhook signature"
1680
+ }
1681
+ });
1682
+ return;
1683
+ }
1684
+ throw error;
1685
+ }
1686
+ });
1687
+ var webhook_default = router6;
1688
+
1689
+ // src/routes/index.ts
1690
+ var router7 = (0, import_express7.Router)();
1691
+ router7.use("/health", health_default);
1692
+ router7.use("/stealth", stealth_default);
1693
+ router7.use("/commitment", commitment_default);
1694
+ router7.use("/proof", proof_default);
1695
+ router7.use("/quote", swap_default);
1696
+ router7.use("/swap", swap_default);
1697
+ router7.use("/webhooks", webhook_default);
1698
+ var routes_default = router7;
1699
+
1700
+ // src/routes/metrics.ts
1701
+ var import_express8 = require("express");
1702
+ var router8 = (0, import_express8.Router)();
1703
+ router8.get("/", async (req, res) => {
1704
+ try {
1705
+ res.set("Content-Type", register.contentType);
1706
+ res.end(await register.metrics());
1707
+ } catch (err) {
1708
+ res.status(500).end();
1709
+ }
1710
+ });
1711
+ var metrics_default = router8;
430
1712
 
431
1713
  // src/server.ts
432
- var app = (0, import_express7.default)();
433
- var PORT = process.env.PORT || 3e3;
434
- var NODE_ENV = process.env.NODE_ENV || "development";
435
- var CORS_ORIGIN = process.env.CORS_ORIGIN || "*";
1714
+ initSentry();
1715
+ var app = (0, import_express9.default)();
1716
+ var trustProxy = env.TRUST_PROXY;
1717
+ if (trustProxy !== "false") {
1718
+ const parsedValue = /^\d+$/.test(trustProxy) ? parseInt(trustProxy, 10) : trustProxy;
1719
+ app.set("trust proxy", parsedValue);
1720
+ logger.info({ trustProxy: parsedValue }, "Proxy trust configured");
1721
+ }
1722
+ if (env.METRICS_ENABLED === "true") {
1723
+ app.use(metricsMiddleware);
1724
+ }
1725
+ app.use(shutdownMiddleware);
1726
+ app.use(requestIdMiddleware);
436
1727
  app.use((0, import_helmet.default)());
437
- app.use((0, import_cors.default)({
438
- origin: CORS_ORIGIN,
439
- credentials: true
440
- }));
441
- app.use(import_express7.default.json({ limit: "1mb" }));
442
- app.use(import_express7.default.urlencoded({ extended: true, limit: "1mb" }));
1728
+ app.use(secureCors);
1729
+ app.use(rateLimiter);
1730
+ app.use(authenticate);
1731
+ app.use(import_express9.default.json({ limit: "1mb" }));
1732
+ app.use(import_express9.default.urlencoded({ extended: true, limit: "1mb" }));
443
1733
  app.use((0, import_compression.default)());
444
- app.use((0, import_morgan.default)(NODE_ENV === "development" ? "dev" : "combined"));
1734
+ app.use(requestLogger);
445
1735
  app.use("/api/v1", routes_default);
1736
+ if (env.METRICS_ENABLED === "true") {
1737
+ app.use("/metrics", metrics_default);
1738
+ }
446
1739
  app.get("/", (req, res) => {
1740
+ const corsConfig = getCorsConfig();
447
1741
  res.json({
448
1742
  name: "@sip-protocol/api",
449
1743
  version: "0.1.0",
@@ -456,28 +1750,75 @@ app.get("/", (req, res) => {
456
1750
  proof: "POST /api/v1/proof/funding",
457
1751
  quote: "POST /api/v1/quote",
458
1752
  swap: "POST /api/v1/swap",
459
- swapStatus: "GET /api/v1/swap/:id/status"
1753
+ swapStatus: "GET /api/v1/swap/:id/status",
1754
+ webhookRegister: "POST /api/v1/webhooks/register",
1755
+ webhookUnregister: "DELETE /api/v1/webhooks/:id",
1756
+ webhookList: "GET /api/v1/webhooks",
1757
+ heliusIngest: "POST /api/v1/webhooks/internal/helius"
1758
+ },
1759
+ security: {
1760
+ authentication: isAuthEnabled() ? "enabled" : "disabled",
1761
+ cors: {
1762
+ origins: corsConfig.origins.length > 0 ? corsConfig.origins.length + " configured" : "none (blocked)",
1763
+ credentials: corsConfig.credentials
1764
+ },
1765
+ rateLimit: "enabled"
460
1766
  }
461
1767
  });
462
1768
  });
463
1769
  app.use(notFoundHandler);
1770
+ setupSentryErrorHandler(app);
464
1771
  app.use(errorHandler);
465
1772
  if (require.main === module) {
466
- app.listen(PORT, () => {
467
- console.log(`
1773
+ logConfigWarnings(logger);
1774
+ const corsConfig = getCorsConfig();
1775
+ const server = app.listen(env.PORT, () => {
1776
+ logger.info({
1777
+ port: env.PORT,
1778
+ environment: env.NODE_ENV,
1779
+ auth: isAuthEnabled() ? "enabled" : "disabled",
1780
+ corsOrigins: corsConfig.origins.length,
1781
+ logLevel: env.LOG_LEVEL,
1782
+ sentry: isSentryEnabled() ? "enabled" : "disabled",
1783
+ metrics: env.METRICS_ENABLED === "true" ? "enabled" : "disabled"
1784
+ }, "SIP Protocol API started");
1785
+ if (env.isDevelopment) {
1786
+ console.log(`
468
1787
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
469
1788
  \u2551 SIP Protocol REST API \u2551
470
1789
  \u2551 Version: 0.1.0 \u2551
471
- \u2551 Port: ${PORT} \u2551
472
- \u2551 Environment: ${NODE_ENV} \u2551
473
- \u2551 Documentation: http://localhost:${PORT}/ \u2551
1790
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
1791
+ \u2551 Port: ${String(env.PORT).padEnd(43)}\u2551
1792
+ \u2551 Environment: ${env.NODE_ENV.padEnd(37)}\u2551
1793
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
1794
+ \u2551 Security: \u2551
1795
+ \u2551 \u2022 Auth: ${(isAuthEnabled() ? "ENABLED" : "disabled (dev mode)").padEnd(42)}\u2551
1796
+ \u2551 \u2022 CORS: ${(corsConfig.origins.length + " origins").padEnd(42)}\u2551
1797
+ \u2551 \u2022 Rate Limit: enabled \u2551
1798
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
1799
+ \u2551 Monitoring: \u2551
1800
+ \u2551 \u2022 Sentry: ${(isSentryEnabled() ? "ENABLED" : "disabled").padEnd(40)}\u2551
1801
+ \u2551 \u2022 Metrics: ${(env.METRICS_ENABLED === "true" ? "/metrics" : "disabled").padEnd(39)}\u2551
1802
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
1803
+ \u2551 Logging: ${env.LOG_LEVEL.padEnd(41)}\u2551
1804
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
1805
+ \u2551 Documentation: http://localhost:${String(env.PORT).padEnd(17)}\u2551
474
1806
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
475
- `);
1807
+ `);
1808
+ }
1809
+ });
1810
+ setupGracefulShutdown(server, async () => {
1811
+ logger.info("Draining webhook deliveries...");
1812
+ await webhookDeliveryService.drainPending();
1813
+ if (isSentryEnabled()) {
1814
+ logger.info("Flushing Sentry events...");
1815
+ await flushSentry(2e3);
1816
+ }
1817
+ logger.info("Flushing logs...");
476
1818
  });
477
1819
  }
478
1820
  var server_default = app;
479
- /*! Bundled license information:
480
-
481
- @noble/hashes/esm/utils.js:
482
- (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
483
- */
1821
+ // Annotate the CommonJS export names for ESM import in node:
1822
+ 0 && (module.exports = {
1823
+ isServerShuttingDown
1824
+ });