hive-intelligence 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/agent-skills/SKILL.md +1 -1
  3. package/agent-skills/packs/hive-build/SKILL.md +1 -1
  4. package/agent-skills/packs/hive-build-onboarding/SKILL.md +7 -7
  5. package/agent-skills/packs/hive-cli/SKILL.md +2 -2
  6. package/agent-skills/packs/hive-defi-research/SKILL.md +1 -1
  7. package/agent-skills/packs/hive-dex-pool-analysis/SKILL.md +1 -1
  8. package/agent-skills/packs/hive-market-research/SKILL.md +1 -1
  9. package/agent-skills/packs/hive-mcp/SKILL.md +1 -1
  10. package/agent-skills/packs/hive-network-infrastructure/SKILL.md +1 -1
  11. package/agent-skills/packs/hive-nft-research/SKILL.md +1 -1
  12. package/agent-skills/packs/hive-prediction-markets/SKILL.md +1 -1
  13. package/agent-skills/packs/hive-query/SKILL.md +1 -1
  14. package/agent-skills/packs/hive-security-risk/SKILL.md +1 -1
  15. package/agent-skills/packs/hive-solana-analysis/SKILL.md +1 -1
  16. package/agent-skills/packs/hive-stateful-monitoring/SKILL.md +1 -1
  17. package/agent-skills/packs/hive-token-diligence/SKILL.md +1 -1
  18. package/agent-skills/packs/hive-tool-discovery/SKILL.md +1 -1
  19. package/agent-skills/packs/hive-wallet-investigation/SKILL.md +1 -1
  20. package/build/{api-client-FTVDFW5V.js → api-client-WMWFWXC7.js} +3 -15
  21. package/build/{auth-EZFJ5RYB.js → auth-HRFQWQMZ.js} +1 -9
  22. package/build/browser-auth-KXIO5BHM.js +20 -0
  23. package/build/{chunk-I5Z6FZRL.js → chunk-3OZFBWFK.js} +14 -12
  24. package/build/{chunk-N32UIHBU.js → chunk-7DYHWY4Y.js} +1 -1
  25. package/build/{chunk-7GD2MFYI.js → chunk-AQQAPAER.js} +35 -15
  26. package/build/{upgrade-J4HIGCKE.js → chunk-GHCYCBA3.js} +1 -2
  27. package/build/{chunk-Q7VBYLX7.js → chunk-P7NLFSZQ.js} +0 -58
  28. package/build/{chunk-RUHO24SX.js → chunk-TVQJY47C.js} +2 -157
  29. package/build/{chunk-W2QL7LYI.js → chunk-UFTNBCSA.js} +885 -101
  30. package/build/cli.js +28 -39
  31. package/build/{doctor-ORDOCGLM.js → doctor-GHUAUFS2.js} +3 -3
  32. package/build/{init-all-BVWW3JVK.js → init-all-RG542NWG.js} +32 -13
  33. package/build/{mcpServer-RUEOTTF7.js → mcpServer-D7OXUDU5.js} +1 -1
  34. package/build/monitor-worker.js +2 -2
  35. package/build/release.json +4 -4
  36. package/build/{serve-DKLR2WQH.js → serve-K3IT7DS4.js} +1 -1
  37. package/build/server.js +1429 -415
  38. package/build/stdio.js +1 -1
  39. package/build/{tools-I7GLWTV6.js → tools-D72ZH6P5.js} +1 -1
  40. package/build/upgrade-4S5XT5JG.js +14 -0
  41. package/build/{watch-WVR4CGCD.js → watch-IOSRSCFE.js} +2 -2
  42. package/package.json +5 -3
  43. package/build/browser-auth-OJSUNFBI.js +0 -8
package/build/server.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  ensureWallet,
7
7
  executeToolByName,
8
8
  getWalletStatus
9
- } from "./chunk-I5Z6FZRL.js";
9
+ } from "./chunk-3OZFBWFK.js";
10
10
  import {
11
11
  CategoryEndpoints,
12
12
  HERO_TOOL_NAMES,
@@ -26,6 +26,7 @@ import {
26
26
  TASK_TOOLSETS,
27
27
  ToolRegistry,
28
28
  buildAuthCompatibility,
29
+ buildStatefulWriteAuditEvent,
29
30
  buildSubjectBodyDigest,
30
31
  buildToolCatalogEntry,
31
32
  clientProfileFromName,
@@ -38,6 +39,7 @@ import {
38
39
  getAnalyticsWriter,
39
40
  getAvailableApis,
40
41
  getCorsAllowedOrigins,
42
+ getCredentialedRateLimitMax,
41
43
  getCsvEnv,
42
44
  getEnvVar,
43
45
  getMcpCreditCost,
@@ -53,12 +55,10 @@ import {
53
55
  getRootVisibleToolCount,
54
56
  getSharedClients,
55
57
  getSupabaseAdmin,
56
- getSupabaseAuthClient,
57
58
  getTaskCanaryCatalog,
58
59
  getTaskCanaryHistory,
59
60
  getWorkflowRegressionCatalog,
60
61
  getWorkflowRegressionHistory,
61
- hasEnvVars,
62
62
  hasSubjectSigningSecret,
63
63
  heroTools,
64
64
  isB2BSubjectsEnabled,
@@ -66,6 +66,7 @@ import {
66
66
  isHiveOAuthAccessToken,
67
67
  isProduction,
68
68
  isRedisAvailable,
69
+ isRedisConfigured,
69
70
  isSupabaseConfigured,
70
71
  isUnkeyConfigured,
71
72
  isUnkeyRequired,
@@ -92,8 +93,9 @@ import {
92
93
  trackOAuthFirstMaterialReceipt,
93
94
  trackServerEvent,
94
95
  validateEndpointArgs,
95
- verifyApiKey
96
- } from "./chunk-W2QL7LYI.js";
96
+ verifyApiKey,
97
+ waitForRedisAvailability
98
+ } from "./chunk-UFTNBCSA.js";
97
99
  import {
98
100
  HIVE_AGENT_SKILLS
99
101
  } from "./chunk-L326MQZP.js";
@@ -733,7 +735,7 @@ var require_ipaddr = __commonJS({
733
735
 
734
736
  // src/server.ts
735
737
  import dotenv from "dotenv";
736
- import { randomUUID as randomUUID4, timingSafeEqual as timingSafeEqual5 } from "crypto";
738
+ import { randomUUID as randomUUID4, timingSafeEqual as timingSafeEqual6 } from "crypto";
737
739
  import { resolve } from "path";
738
740
  import { fileURLToPath } from "url";
739
741
  import { createRequire as createNodeRequire } from "module";
@@ -741,7 +743,7 @@ import express2 from "express";
741
743
  import compression from "compression";
742
744
  import cors from "cors";
743
745
  import helmet from "helmet";
744
- import { z as z6 } from "zod";
746
+ import { z as z5 } from "zod";
745
747
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
746
748
 
747
749
  // src/middleware/rate-limit.ts
@@ -805,8 +807,43 @@ function ipRateLimitKeyFromRequest(req) {
805
807
  }
806
808
 
807
809
  // src/middleware/rate-limit.ts
808
- var createIpRateLimiter = () => {
809
- const maxRequests = getRateLimitMax();
810
+ var STORE_ERROR_LOG_INTERVAL_MS = 6e4;
811
+ var lastStoreErrorLoggedAt = 0;
812
+ var suppressedStoreErrors = 0;
813
+ function noteRateLimitStoreError(error) {
814
+ suppressedStoreErrors += 1;
815
+ const now = Date.now();
816
+ if (now - lastStoreErrorLoggedAt < STORE_ERROR_LOG_INTERVAL_MS) return;
817
+ lastStoreErrorLoggedAt = now;
818
+ const suppressed = suppressedStoreErrors;
819
+ suppressedStoreErrors = 0;
820
+ secureLogger.error(
821
+ "[rate-limit] Redis store error \u2014 requests are passing UNLIMITED until it recovers",
822
+ {
823
+ errorMessage: error instanceof Error ? error.message : String(error),
824
+ occurrencesSinceLastLog: suppressed
825
+ }
826
+ );
827
+ }
828
+ function isRateLimiterDegraded() {
829
+ return lastStoreErrorLoggedAt > 0 && Date.now() - lastStoreErrorLoggedAt < STORE_ERROR_LOG_INTERVAL_MS * 2;
830
+ }
831
+ function hasValidMonitorSecret(req) {
832
+ const secret = getEnvVar("MONITOR_SECRET");
833
+ const provided = req.headers["x-monitor-secret"];
834
+ if (!secret || typeof provided !== "string") return false;
835
+ const a = Buffer.from(provided);
836
+ const b = Buffer.from(secret);
837
+ return a.length === b.length && timingSafeEqual(a, b);
838
+ }
839
+ function hasCredential(req) {
840
+ if (typeof req.headers["x-api-key"] === "string") return true;
841
+ const auth = req.headers.authorization;
842
+ return typeof auth === "string" && auth.startsWith("Bearer ");
843
+ }
844
+ var createIpRateLimiter = (opts = {}) => {
845
+ const lane = opts.lane ?? "anonymous";
846
+ const maxRequests = lane === "credentialed" ? getCredentialedRateLimitMax() : getRateLimitMax();
810
847
  const redisClient = getRedisClient();
811
848
  const isProduction2 = getNodeEnv() === "production";
812
849
  if (isProduction2 && !redisClient) {
@@ -824,11 +861,11 @@ var createIpRateLimiter = () => {
824
861
  }
825
862
  const storeConfig = redisClient ? {
826
863
  store: new RedisStore({
827
- sendCommand: ((...args) => redisClient.call(
828
- args[0],
829
- ...args.slice(1)
830
- )),
831
- prefix: "rl:ip:"
864
+ sendCommand: ((...args) => redisClient.call(args[0], ...args.slice(1)).catch((error) => {
865
+ noteRateLimitStoreError(error);
866
+ throw error;
867
+ })),
868
+ prefix: lane === "credentialed" ? "rl:ip:cred:" : "rl:ip:"
832
869
  })
833
870
  } : {};
834
871
  return rateLimit({
@@ -837,14 +874,15 @@ var createIpRateLimiter = () => {
837
874
  max: maxRequests,
838
875
  standardHeaders: true,
839
876
  legacyHeaders: false,
877
+ // Availability over enforcement. A Redis outage or an exhausted provider
878
+ // quota must degrade to unlimited traffic, never to a 500 on every route.
879
+ // On 2026-08-19 an Upstash quota exhaustion took the whole service down
880
+ // for ~16h because the store error propagated to Express.
881
+ passOnStoreError: true,
840
882
  keyGenerator: (req) => ipRateLimitKeyFromRequest(req),
841
883
  skip: (req) => {
842
- const secret = getEnvVar("MONITOR_SECRET");
843
- const provided = req.headers["x-monitor-secret"];
844
- if (!secret || typeof provided !== "string") return false;
845
- const a = Buffer.from(provided);
846
- const b = Buffer.from(secret);
847
- return a.length === b.length && timingSafeEqual(a, b);
884
+ if (hasValidMonitorSecret(req)) return true;
885
+ return lane === "credentialed" ? !hasCredential(req) : hasCredential(req);
848
886
  },
849
887
  handler: (_req, res) => {
850
888
  res.setHeader("Retry-After", "60");
@@ -858,6 +896,24 @@ var createIpRateLimiter = () => {
858
896
  }
859
897
  });
860
898
  };
899
+ var createLocalRateLimiter = (maxPerMinute = 120) => rateLimit({
900
+ windowMs: 60 * 1e3,
901
+ max: maxPerMinute,
902
+ standardHeaders: true,
903
+ legacyHeaders: false,
904
+ keyGenerator: (req) => ipRateLimitKeyFromRequest(req),
905
+ skip: hasValidMonitorSecret,
906
+ handler: (_req, res) => {
907
+ res.setHeader("Retry-After", "60");
908
+ res.status(429).json(
909
+ apiError(
910
+ "rate_limit_error",
911
+ "RATE_LIMITED",
912
+ `Rate limit exceeded. Maximum ${maxPerMinute} requests per minute.`
913
+ )
914
+ );
915
+ }
916
+ });
861
917
 
862
918
  // src/health/index.ts
863
919
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
@@ -1521,8 +1577,16 @@ var PUBLIC_STATUS_DIMENSIONS = [
1521
1577
  "taskCanaries",
1522
1578
  "workflowRouting",
1523
1579
  "mcpProtocol",
1524
- "catalogIntegrity"
1580
+ "catalogIntegrity",
1581
+ "rateLimiter"
1525
1582
  ];
1583
+ function readRateLimiterSignal() {
1584
+ return {
1585
+ configured: isRedisConfigured(),
1586
+ available: isRedisAvailable(),
1587
+ degraded: isRateLimiterDegraded()
1588
+ };
1589
+ }
1526
1590
  var CATALOG_CHECK_NAMES = [
1527
1591
  "mcp_initialize",
1528
1592
  "tools_list_root_surface",
@@ -1646,6 +1710,56 @@ function buildCatalogIntegrityDimension(history) {
1646
1710
  }
1647
1711
  };
1648
1712
  }
1713
+ var RATE_LIMITER_SCOPE = "This instance's shared rate-limit store, upstream response cache, and idempotency keys.";
1714
+ function buildRateLimiterDimension(signal) {
1715
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
1716
+ if (!signal.configured) {
1717
+ return {
1718
+ status: "healthy",
1719
+ scope: RATE_LIMITER_SCOPE,
1720
+ evidence: {
1721
+ state: "absent",
1722
+ source: "serving_instance_redis",
1723
+ observedAt,
1724
+ message: "No REDIS_URL on this instance, so limits, cache and idempotency are per-instance and in memory by design."
1725
+ }
1726
+ };
1727
+ }
1728
+ if (!signal.available) {
1729
+ return {
1730
+ status: "unhealthy",
1731
+ scope: RATE_LIMITER_SCOPE,
1732
+ evidence: {
1733
+ state: "storage_unavailable",
1734
+ source: "serving_instance_redis",
1735
+ observedAt,
1736
+ message: "Redis is configured but this instance has no live connection: IP and per-user rate limits are passing UNLIMITED, and the upstream cache and idempotency store are bypassed."
1737
+ }
1738
+ };
1739
+ }
1740
+ if (signal.degraded) {
1741
+ return {
1742
+ status: "degraded",
1743
+ scope: RATE_LIMITER_SCOPE,
1744
+ evidence: {
1745
+ state: "observed",
1746
+ source: "serving_instance_redis",
1747
+ observedAt,
1748
+ message: "The rate-limit store returned errors recently; those requests failed open and were not counted."
1749
+ }
1750
+ };
1751
+ }
1752
+ return {
1753
+ status: "healthy",
1754
+ scope: RATE_LIMITER_SCOPE,
1755
+ evidence: {
1756
+ state: "observed",
1757
+ source: "serving_instance_redis",
1758
+ observedAt,
1759
+ message: "This instance holds a live connection to the rate-limit store."
1760
+ }
1761
+ };
1762
+ }
1649
1763
  function overallStatus(dimensions) {
1650
1764
  const required = [
1651
1765
  dimensions.providers,
@@ -1653,7 +1767,8 @@ function overallStatus(dimensions) {
1653
1767
  dimensions.workflowRouting,
1654
1768
  dimensions.mcpProtocol,
1655
1769
  dimensions.oauthLifecycle,
1656
- dimensions.catalogIntegrity
1770
+ dimensions.catalogIntegrity,
1771
+ dimensions.rateLimiter
1657
1772
  ];
1658
1773
  if (required.some((dimension) => dimension.status === "unhealthy")) {
1659
1774
  return "unhealthy";
@@ -1666,7 +1781,7 @@ function overallStatus(dimensions) {
1666
1781
  }
1667
1782
  return "healthy";
1668
1783
  }
1669
- function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, protocol, oauthLifecycle) {
1784
+ function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, protocol, oauthLifecycle, rateLimiter = readRateLimiterSignal()) {
1670
1785
  const results = providers.latest ?? {
1671
1786
  status: "unknown",
1672
1787
  totalApis: 0,
@@ -1725,7 +1840,8 @@ function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, pro
1725
1840
  },
1726
1841
  mcpProtocol: buildMcpProtocolDimension(protocol),
1727
1842
  oauthLifecycle: buildOAuthLifecycleDimension(oauthLifecycle),
1728
- catalogIntegrity: buildCatalogIntegrityDimension(protocol)
1843
+ catalogIntegrity: buildCatalogIntegrityDimension(protocol),
1844
+ rateLimiter: buildRateLimiterDimension(rateLimiter)
1729
1845
  };
1730
1846
  return {
1731
1847
  ...results,
@@ -1749,7 +1865,8 @@ function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, pro
1749
1865
  "workflowRouting",
1750
1866
  "mcpProtocol",
1751
1867
  "oauthLifecycle",
1752
- "catalogIntegrity"
1868
+ "catalogIntegrity",
1869
+ "rateLimiter"
1753
1870
  ],
1754
1871
  rule: "Any unhealthy dimension makes overall status unhealthy; otherwise degraded wins, missing evidence remains unknown, and healthy requires every required dimension to be healthy."
1755
1872
  }
@@ -2353,8 +2470,91 @@ var getCorsConfig = () => {
2353
2470
 
2354
2471
  // src/middleware/api-key.ts
2355
2472
  import { createHash, randomUUID } from "crypto";
2473
+
2474
+ // src/services/user-rate-limit.ts
2475
+ var WINDOW_MS = 6e4;
2476
+ var WINDOW_SECONDS = 60;
2477
+ var PLAN_CACHE_TTL_MS = 6e4;
2478
+ var planCache = /* @__PURE__ */ new Map();
2479
+ var FALLBACK_LIMIT_PER_MIN = 100;
2480
+ var localCounters = /* @__PURE__ */ new Map();
2481
+ async function resolveUserRateLimit(userId) {
2482
+ const cached = planCache.get(userId);
2483
+ if (cached && cached.expiresAt > Date.now()) return cached.limit;
2484
+ let limit = FALLBACK_LIMIT_PER_MIN;
2485
+ try {
2486
+ const supabase = getSupabaseAdmin();
2487
+ const { data: profile } = await supabase.from("user_profiles").select("plan_id").eq("id", userId).single();
2488
+ const planId = profile?.plan_id ?? "demo";
2489
+ const { data: plan } = await supabase.from("plans").select("rate_limit_per_min").eq("id", planId).single();
2490
+ if (typeof plan?.rate_limit_per_min === "number") {
2491
+ limit = plan.rate_limit_per_min;
2492
+ }
2493
+ } catch (error) {
2494
+ secureLogger.warn("Plan rate lookup failed \u2014 using the free-tier rate", {
2495
+ errorMessage: error instanceof Error ? error.message : String(error)
2496
+ });
2497
+ }
2498
+ planCache.set(userId, { limit, expiresAt: Date.now() + PLAN_CACHE_TTL_MS });
2499
+ return limit;
2500
+ }
2501
+ function consumeLocal(key, limit) {
2502
+ const now = Date.now();
2503
+ const existing = localCounters.get(key);
2504
+ const entry = existing && existing.expiresAt > now ? existing : { count: 0, expiresAt: now + WINDOW_MS };
2505
+ entry.count += 1;
2506
+ localCounters.set(key, entry);
2507
+ const resetSeconds = Math.max(1, Math.ceil((entry.expiresAt - now) / 1e3));
2508
+ return {
2509
+ allowed: entry.count <= limit,
2510
+ limit,
2511
+ remaining: Math.max(0, limit - entry.count),
2512
+ resetSeconds,
2513
+ degraded: false
2514
+ };
2515
+ }
2516
+ async function consumeUserRateLimit(userId, limitPerMin) {
2517
+ const window = Math.floor(Date.now() / WINDOW_MS);
2518
+ const key = `rl:user:${userId}:${window}`;
2519
+ const redis = getRedisClient();
2520
+ if (!redis) return consumeLocal(key, limitPerMin);
2521
+ try {
2522
+ const count = await redis.incr(key);
2523
+ if (count === 1) await redis.expire(key, WINDOW_SECONDS);
2524
+ const resetSeconds = Math.max(
2525
+ 1,
2526
+ WINDOW_SECONDS - Math.floor(Date.now() % WINDOW_MS / 1e3)
2527
+ );
2528
+ return {
2529
+ allowed: count <= limitPerMin,
2530
+ limit: limitPerMin,
2531
+ remaining: Math.max(0, limitPerMin - count),
2532
+ resetSeconds,
2533
+ degraded: false
2534
+ };
2535
+ } catch (error) {
2536
+ secureLogger.warn(
2537
+ "[user-rate-limit] store error \u2014 request passed UNLIMITED",
2538
+ { errorMessage: error instanceof Error ? error.message : String(error) }
2539
+ );
2540
+ return {
2541
+ allowed: true,
2542
+ limit: limitPerMin,
2543
+ remaining: limitPerMin,
2544
+ resetSeconds: WINDOW_SECONDS,
2545
+ degraded: true
2546
+ };
2547
+ }
2548
+ }
2549
+ async function enforceUserRateLimit(userId) {
2550
+ const limit = await resolveUserRateLimit(userId);
2551
+ return consumeUserRateLimit(userId, limit);
2552
+ }
2553
+
2554
+ // src/middleware/api-key.ts
2356
2555
  var REQUEST_COST = 1;
2357
- function attachVerifiedContext(req, res, key, result, quotaRemaining) {
2556
+ var REST_QUOTA_EXHAUSTED_MESSAGE = "Monthly credit quota exhausted. Upgrade at https://www.hiveintelligence.xyz/pricing to raise the limit now, or wait for the next cycle.";
2557
+ function attachVerifiedContext(req, res, key, result, quotaRemaining, rateDecision) {
2358
2558
  const authReq = req;
2359
2559
  authReq.apiKey = key;
2360
2560
  authReq.apiKeyEnvironment = getKeyEnvironment(key);
@@ -2362,7 +2562,7 @@ function attachVerifiedContext(req, res, key, result, quotaRemaining) {
2362
2562
  authReq.planId = result.planId;
2363
2563
  authReq.unkeyKeyId = result.keyId;
2364
2564
  authReq.quotaRemaining = quotaRemaining ?? void 0;
2365
- authReq.ratelimitRemaining = result.ratelimit?.remaining;
2565
+ authReq.ratelimitRemaining = rateDecision?.remaining ?? result.ratelimit?.remaining;
2366
2566
  authReq.unkeyResult = result;
2367
2567
  if (quotaRemaining !== void 0) {
2368
2568
  res.setHeader(
@@ -2370,9 +2570,11 @@ function attachVerifiedContext(req, res, key, result, quotaRemaining) {
2370
2570
  quotaRemaining === null ? "unlimited" : String(quotaRemaining)
2371
2571
  );
2372
2572
  }
2373
- if (result.ratelimit) {
2374
- res.setHeader("X-RateLimit-Remaining", String(result.ratelimit.remaining));
2375
- res.setHeader("X-RateLimit-Limit", String(result.ratelimit.limit));
2573
+ const limit = rateDecision?.limit ?? result.ratelimit?.limit;
2574
+ const remaining = rateDecision?.remaining ?? result.ratelimit?.remaining;
2575
+ if (limit !== void 0 && remaining !== void 0) {
2576
+ res.setHeader("X-RateLimit-Remaining", String(remaining));
2577
+ res.setHeader("X-RateLimit-Limit", String(limit));
2376
2578
  }
2377
2579
  }
2378
2580
  function deferApiKeyCreditDebit(req, _res, next) {
@@ -2397,7 +2599,7 @@ async function debitApiKeyCreditForExecution(req, res) {
2397
2599
  return {
2398
2600
  type: "rate_limit_error",
2399
2601
  code: "QUOTA_EXCEEDED",
2400
- message: "Monthly credit quota exhausted. Upgrade your plan or wait for the next cycle.",
2602
+ message: REST_QUOTA_EXHAUSTED_MESSAGE,
2401
2603
  runtimeStatus: "rate_limited"
2402
2604
  };
2403
2605
  }
@@ -2529,8 +2731,25 @@ function requireApiKey(req, res, next) {
2529
2731
  );
2530
2732
  return;
2531
2733
  }
2734
+ const rateDecision = await enforceUserRateLimit(result.userId);
2735
+ res.setHeader("RateLimit-Limit", String(rateDecision.limit));
2736
+ res.setHeader("RateLimit-Remaining", String(rateDecision.remaining));
2737
+ res.setHeader("RateLimit-Reset", String(rateDecision.resetSeconds));
2738
+ res.setHeader("RateLimit-Policy", `${rateDecision.limit};w=60`);
2739
+ if (!rateDecision.allowed) {
2740
+ res.setHeader("Retry-After", String(rateDecision.resetSeconds));
2741
+ res.status(429).json(
2742
+ apiError(
2743
+ "rate_limit_error",
2744
+ "RATE_LIMITED",
2745
+ `Rate limit exceeded: ${rateDecision.limit} requests per minute on your plan. This limit is shared across all of your API keys and connected clients.`,
2746
+ { requestId: req.requestId }
2747
+ )
2748
+ );
2749
+ return;
2750
+ }
2532
2751
  if (req.deferCreditDebit) {
2533
- attachVerifiedContext(req, res, key, result);
2752
+ attachVerifiedContext(req, res, key, result, void 0, rateDecision);
2534
2753
  next();
2535
2754
  return;
2536
2755
  }
@@ -2552,7 +2771,7 @@ function requireApiKey(req, res, next) {
2552
2771
  apiError(
2553
2772
  "rate_limit_error",
2554
2773
  "QUOTA_EXCEEDED",
2555
- "Monthly credit quota exhausted. Upgrade your plan or wait for the next cycle.",
2774
+ REST_QUOTA_EXHAUSTED_MESSAGE,
2556
2775
  { requestId: req.requestId }
2557
2776
  )
2558
2777
  );
@@ -2573,7 +2792,14 @@ function requireApiKey(req, res, next) {
2573
2792
  );
2574
2793
  return;
2575
2794
  }
2576
- attachVerifiedContext(req, res, key, result, consumeResult.balance);
2795
+ attachVerifiedContext(
2796
+ req,
2797
+ res,
2798
+ key,
2799
+ result,
2800
+ consumeResult.balance,
2801
+ rateDecision
2802
+ );
2577
2803
  next();
2578
2804
  } catch (err) {
2579
2805
  secureLogger.error("consume_credits threw \u2014 rejecting request", err, {
@@ -2666,9 +2892,6 @@ function requireApiKey(req, res, next) {
2666
2892
 
2667
2893
  // src/routes/auth.ts
2668
2894
  import { Router as Router2 } from "express";
2669
- import rateLimit2 from "express-rate-limit";
2670
- import RedisStore2 from "rate-limit-redis";
2671
- import { z } from "zod";
2672
2895
 
2673
2896
  // src/middleware/supabase-auth.ts
2674
2897
  function requireSupabaseAuth(req, res, next) {
@@ -2728,136 +2951,6 @@ var asyncHandler = (fn) => (req, res, next) => {
2728
2951
 
2729
2952
  // src/routes/auth.ts
2730
2953
  var router2 = Router2();
2731
- function buildAuthRateLimiter(maxPerHour, prefix) {
2732
- const redisClient = getRedisClient();
2733
- const storeConfig = redisClient ? {
2734
- store: new RedisStore2({
2735
- sendCommand: ((...args) => redisClient.call(
2736
- args[0],
2737
- ...args.slice(1)
2738
- )),
2739
- prefix
2740
- })
2741
- } : {};
2742
- return rateLimit2({
2743
- ...storeConfig,
2744
- windowMs: 60 * 60 * 1e3,
2745
- max: maxPerHour,
2746
- standardHeaders: true,
2747
- legacyHeaders: false,
2748
- keyGenerator: (req) => ipRateLimitKeyFromRequest(req),
2749
- handler: (_req, res) => {
2750
- res.setHeader("Retry-After", "3600");
2751
- res.status(429).json(
2752
- apiError(
2753
- "rate_limit_error",
2754
- "RATE_LIMITED",
2755
- "Too many auth attempts. Try again in an hour."
2756
- )
2757
- );
2758
- }
2759
- });
2760
- }
2761
- var signupRateLimit = buildAuthRateLimiter(10, "rl:auth:signup:");
2762
- var loginRateLimit = buildAuthRateLimiter(30, "rl:auth:login:");
2763
- var AuthSchema = z.object({
2764
- email: z.string().email("Invalid email format"),
2765
- password: z.string().min(8, "Password must be at least 8 characters")
2766
- });
2767
- router2.post(
2768
- "/signup",
2769
- signupRateLimit,
2770
- asyncHandler(async (req, res) => {
2771
- if (!isSupabaseConfigured() || !hasEnvVars("SUPABASE_ANON_KEY")) {
2772
- return res.status(503).json(
2773
- apiError(
2774
- "internal_error",
2775
- "AUTH_NOT_CONFIGURED",
2776
- "Auth not configured."
2777
- )
2778
- );
2779
- }
2780
- const parseResult = AuthSchema.safeParse(req.body);
2781
- if (!parseResult.success) {
2782
- return res.status(400).json(
2783
- apiError(
2784
- "invalid_request_error",
2785
- "VALIDATION_ERROR",
2786
- parseResult.error.issues.map((e) => e.message).join("; ")
2787
- )
2788
- );
2789
- }
2790
- const { email, password } = parseResult.data;
2791
- const { data, error } = await getSupabaseAuthClient().auth.signUp({
2792
- email,
2793
- password
2794
- });
2795
- if (error || !data.user) {
2796
- return res.status(400).json(
2797
- apiError(
2798
- "authentication_error",
2799
- "SIGNUP_FAILED",
2800
- "Signup failed. Please try again or use a different email."
2801
- )
2802
- );
2803
- }
2804
- return res.status(201).json({
2805
- ok: true,
2806
- user: { id: data.user.id, email: data.user.email },
2807
- requires_email_confirmation: true,
2808
- session: null,
2809
- api_key: null
2810
- });
2811
- })
2812
- );
2813
- router2.post(
2814
- "/login",
2815
- loginRateLimit,
2816
- asyncHandler(async (req, res) => {
2817
- if (!isSupabaseConfigured() || !hasEnvVars("SUPABASE_ANON_KEY")) {
2818
- return res.status(503).json(
2819
- apiError(
2820
- "internal_error",
2821
- "AUTH_NOT_CONFIGURED",
2822
- "Auth not configured."
2823
- )
2824
- );
2825
- }
2826
- const parseResult = AuthSchema.safeParse(req.body);
2827
- if (!parseResult.success) {
2828
- return res.status(400).json(
2829
- apiError(
2830
- "invalid_request_error",
2831
- "VALIDATION_ERROR",
2832
- parseResult.error.issues.map((e) => e.message).join("; ")
2833
- )
2834
- );
2835
- }
2836
- const { email, password } = parseResult.data;
2837
- const { data, error } = await getSupabaseAuthClient().auth.signInWithPassword({
2838
- email,
2839
- password
2840
- });
2841
- if (error || !data.session) {
2842
- return res.status(401).json(
2843
- apiError(
2844
- "authentication_error",
2845
- "LOGIN_FAILED",
2846
- error?.message ?? "Invalid credentials."
2847
- )
2848
- );
2849
- }
2850
- return res.json({
2851
- ok: true,
2852
- user: { id: data.user.id, email: data.user.email },
2853
- session: {
2854
- access_token: data.session.access_token,
2855
- refresh_token: data.session.refresh_token,
2856
- expires_at: data.session.expires_at
2857
- }
2858
- });
2859
- })
2860
- );
2861
2954
  router2.get(
2862
2955
  "/me",
2863
2956
  requireSupabaseAuth,
@@ -2875,7 +2968,7 @@ var auth_default = router2;
2875
2968
 
2876
2969
  // src/routes/keys.ts
2877
2970
  import { Router as Router3 } from "express";
2878
- import { z as z2 } from "zod";
2971
+ import { z } from "zod";
2879
2972
 
2880
2973
  // src/middleware/idempotency.ts
2881
2974
  import { createHash as createHash2 } from "crypto";
@@ -3208,9 +3301,9 @@ async function handleRedisLookup(key, requestFingerprint, req, res, next) {
3208
3301
 
3209
3302
  // src/routes/keys.ts
3210
3303
  var router3 = Router3();
3211
- var CreateKeySchema = z2.object({
3212
- name: z2.string().min(1).max(100).optional(),
3213
- b2b_subjects_enabled: z2.boolean().optional()
3304
+ var CreateKeySchema = z.object({
3305
+ name: z.string().min(1).max(100).optional(),
3306
+ b2b_subjects_enabled: z.boolean().optional()
3214
3307
  });
3215
3308
  router3.post(
3216
3309
  "/",
@@ -3375,6 +3468,7 @@ router4.get(
3375
3468
  const { data: plan } = await getSupabaseAdmin().from("plans").select("*").eq("id", resolvedPlanId).single();
3376
3469
  await ensureWallet(userId, resolvedPlanId);
3377
3470
  const wallet = await getWalletStatus(userId);
3471
+ const { data: subscription } = await getSupabaseAdmin().from("stripe_subscriptions").select("status, current_period_start, current_period_end").eq("user_id", userId).in("status", ["trialing", "active", "past_due", "unpaid"]).order("current_period_end", { ascending: false }).limit(1).maybeSingle();
3378
3472
  const keys = await listKeys(userId);
3379
3473
  const keyEntries = keys.map((k) => ({
3380
3474
  key_id: k.keyId,
@@ -3398,6 +3492,11 @@ router4.get(
3398
3492
  cycle_start: wallet.cycleStart,
3399
3493
  cycle_end: wallet.cycleEnd
3400
3494
  } : null,
3495
+ subscription: subscription ? {
3496
+ status: subscription.status,
3497
+ current_period_start: subscription.current_period_start,
3498
+ current_period_end: subscription.current_period_end
3499
+ } : null,
3401
3500
  keys: keyEntries,
3402
3501
  total_keys: keys.length
3403
3502
  }
@@ -3588,13 +3687,13 @@ var b2b_default = router5;
3588
3687
  import {
3589
3688
  Router as Router6
3590
3689
  } from "express";
3591
- import { z as z3 } from "zod";
3690
+ import { z as z2 } from "zod";
3592
3691
 
3593
3692
  // src/services/stripe-service.ts
3594
3693
  import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
3595
3694
  var STRIPE_API_BASE = "https://api.stripe.com";
3596
3695
  var STRIPE_SIGNATURE_TOLERANCE_SECONDS = 300;
3597
- var PAID_PLAN_ID = "pro";
3696
+ var PAID_PLAN_ID = "analyst";
3598
3697
  var FREE_PLAN_ID = "demo";
3599
3698
  function getStripeSecretKey() {
3600
3699
  return getEnvVar("STRIPE_SECRET_KEY");
@@ -3605,16 +3704,44 @@ function getStripeWebhookSecret() {
3605
3704
  function isStripeCheckoutConfigured() {
3606
3705
  return Boolean(getStripeSecretKey());
3607
3706
  }
3608
- function priceIdForCycle(cycle) {
3609
- return cycle === "yearly" ? getEnvVar("STRIPE_PRICE_PRO_YEARLY") : getEnvVar("STRIPE_PRICE_PRO_MONTHLY");
3707
+ var STRIPE_PRICE_ENV = {
3708
+ starter: {
3709
+ monthly: "STRIPE_PRICE_STARTER_MONTHLY",
3710
+ yearly: "STRIPE_PRICE_STARTER_YEARLY"
3711
+ },
3712
+ analyst: {
3713
+ monthly: "STRIPE_PRICE_PRO_MONTHLY",
3714
+ yearly: "STRIPE_PRICE_PRO_YEARLY"
3715
+ }
3716
+ };
3717
+ function priceIdForPlanCycle(planId, cycle) {
3718
+ const row = STRIPE_PRICE_ENV[planId];
3719
+ if (!row) return void 0;
3720
+ return getEnvVar(row[cycle]);
3610
3721
  }
3611
3722
  function planIdForPrice(priceId) {
3612
3723
  if (!priceId) return null;
3613
- if (priceId === getEnvVar("STRIPE_PRICE_PRO_MONTHLY") || priceId === getEnvVar("STRIPE_PRICE_PRO_YEARLY")) {
3614
- return PAID_PLAN_ID;
3724
+ for (const [planId, cycles] of Object.entries(STRIPE_PRICE_ENV)) {
3725
+ for (const envName of Object.values(cycles)) {
3726
+ if (priceId === getEnvVar(envName)) return planId;
3727
+ }
3615
3728
  }
3616
3729
  return null;
3617
3730
  }
3731
+ var STRIPE_SUBSCRIPTION_STATUSES = [
3732
+ "incomplete",
3733
+ "incomplete_expired",
3734
+ "trialing",
3735
+ "active",
3736
+ "past_due",
3737
+ "canceled",
3738
+ "unpaid",
3739
+ "paused"
3740
+ ];
3741
+ function registrySubscriptionStatus(status) {
3742
+ if (!status) return null;
3743
+ return STRIPE_SUBSCRIPTION_STATUSES.includes(status) ? status : null;
3744
+ }
3618
3745
  function getBillingSuccessUrl() {
3619
3746
  return getEnvVar("BILLING_SUCCESS_URL") ?? "https://www.hiveintelligence.xyz/dashboard?checkout=success";
3620
3747
  }
@@ -3734,6 +3861,164 @@ async function createStripeCheckoutSession(args) {
3734
3861
  return { id, url };
3735
3862
  }
3736
3863
 
3864
+ // src/services/llamapay-service.ts
3865
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual4 } from "crypto";
3866
+ var LLAMAPAY_API_BASE = "https://api.llamapay.io";
3867
+ var LLAMAPAY_SIGNATURE_HEADER = "x-cc-webhook-signature";
3868
+ function getLlamaPayApiKey() {
3869
+ const key = process.env.LLAMAPAY_API_KEY?.trim();
3870
+ return key && key.length > 0 ? key : void 0;
3871
+ }
3872
+ function getLlamaPayWebhookSecret() {
3873
+ const secret = process.env.LLAMAPAY_WEBHOOK_SECRET?.trim();
3874
+ return secret && secret.length > 0 ? secret : void 0;
3875
+ }
3876
+ function isLlamaPayCheckoutConfigured() {
3877
+ return getLlamaPayApiKey() !== void 0;
3878
+ }
3879
+ var PLAN_PRICES_USD = {
3880
+ starter: { monthly: 49, yearly: 490 },
3881
+ analyst: { monthly: 149, yearly: 1490 }
3882
+ };
3883
+ var PLAN_TIER_ORDER = [
3884
+ "demo",
3885
+ "starter",
3886
+ "analyst",
3887
+ "enterprise"
3888
+ ];
3889
+ function isSellablePlan(planId) {
3890
+ return Object.prototype.hasOwnProperty.call(PLAN_PRICES_USD, planId);
3891
+ }
3892
+ function priceForPlanCycle(planId, cycle) {
3893
+ const row = PLAN_PRICES_USD[planId];
3894
+ if (!row) return void 0;
3895
+ return cycle === "yearly" ? row.yearly : row.monthly;
3896
+ }
3897
+ function canUpgradeTo(current, target) {
3898
+ const currentIndex = PLAN_TIER_ORDER.indexOf(current ?? "demo");
3899
+ const targetIndex = PLAN_TIER_ORDER.indexOf(target);
3900
+ if (targetIndex < 0) return false;
3901
+ return targetIndex > (currentIndex < 0 ? 0 : currentIndex);
3902
+ }
3903
+ var RENEWAL_WINDOW_DAYS = 30;
3904
+ function canPurchasePlan(current, target, termEnd, now = /* @__PURE__ */ new Date()) {
3905
+ if (canUpgradeTo(current, target)) return true;
3906
+ if (current !== target) return false;
3907
+ if (!termEnd) return false;
3908
+ const end = termEnd instanceof Date ? termEnd : new Date(termEnd);
3909
+ if (Number.isNaN(end.getTime())) return false;
3910
+ const windowOpens = new Date(
3911
+ end.getTime() - RENEWAL_WINDOW_DAYS * 24 * 60 * 60 * 1e3
3912
+ );
3913
+ return now.getTime() >= windowOpens.getTime();
3914
+ }
3915
+ function pricingTypeFor(cycle) {
3916
+ return cycle === "yearly" ? "fixed_price" : "subscription";
3917
+ }
3918
+ function addUtcMonths(start, months) {
3919
+ const end = new Date(start.getTime());
3920
+ const day = end.getUTCDate();
3921
+ end.setUTCDate(1);
3922
+ end.setUTCMonth(end.getUTCMonth() + months);
3923
+ const daysInTargetMonth = new Date(
3924
+ Date.UTC(end.getUTCFullYear(), end.getUTCMonth() + 1, 0)
3925
+ ).getUTCDate();
3926
+ end.setUTCDate(Math.min(day, daysInTargetMonth));
3927
+ return end;
3928
+ }
3929
+ function creditCycleEndFrom(start) {
3930
+ return addUtcMonths(start, 1);
3931
+ }
3932
+ function termEndFrom(start, cycle) {
3933
+ return addUtcMonths(start, cycle === "yearly" ? 12 : 1);
3934
+ }
3935
+ var LlamaPayApiError = class extends Error {
3936
+ constructor(status, message) {
3937
+ super(message);
3938
+ this.status = status;
3939
+ this.name = "LlamaPayApiError";
3940
+ }
3941
+ status;
3942
+ };
3943
+ async function createLlamaPayCharge(args) {
3944
+ const apiKey = getLlamaPayApiKey();
3945
+ if (!apiKey) {
3946
+ throw new LlamaPayApiError(503, "LlamaPay API key is not configured");
3947
+ }
3948
+ let response;
3949
+ try {
3950
+ response = await fetch(`${LLAMAPAY_API_BASE}/charges`, {
3951
+ method: "POST",
3952
+ headers: {
3953
+ Authorization: apiKey,
3954
+ "Content-Type": "application/json"
3955
+ },
3956
+ body: JSON.stringify({
3957
+ pricing_type: pricingTypeFor(args.cycle),
3958
+ local_price: {
3959
+ amount: args.amountUsd.toFixed(2),
3960
+ currency: "USD"
3961
+ },
3962
+ metadata: {
3963
+ user_id: args.userId,
3964
+ plan_id: args.planId,
3965
+ billing_cycle: args.cycle,
3966
+ ...args.email ? { email: args.email } : {}
3967
+ },
3968
+ redirect_url: getBillingSuccessUrl(),
3969
+ cancel_url: getBillingCancelUrl()
3970
+ })
3971
+ });
3972
+ } catch (err) {
3973
+ throw new LlamaPayApiError(
3974
+ 502,
3975
+ `LlamaPay request failed: ${err instanceof Error ? err.message : "unknown"}`
3976
+ );
3977
+ }
3978
+ const text = await response.text();
3979
+ if (!response.ok) {
3980
+ throw new LlamaPayApiError(
3981
+ response.status,
3982
+ `LlamaPay charge creation failed (${response.status}): ${text.slice(0, 300)}`
3983
+ );
3984
+ }
3985
+ let payload;
3986
+ try {
3987
+ payload = JSON.parse(text);
3988
+ } catch {
3989
+ throw new LlamaPayApiError(502, "LlamaPay returned a non-JSON response");
3990
+ }
3991
+ const id = payload.data?.id;
3992
+ const hostedUrl = payload.data?.hosted_url;
3993
+ if (typeof id !== "string" || typeof hostedUrl !== "string" || !hostedUrl) {
3994
+ throw new LlamaPayApiError(
3995
+ 502,
3996
+ "LlamaPay charge creation returned no hosted_url"
3997
+ );
3998
+ }
3999
+ return { id, hostedUrl };
4000
+ }
4001
+ function verifyLlamaPaySignature(args) {
4002
+ const header = args.header;
4003
+ if (typeof header !== "string" || header.length === 0) {
4004
+ return { valid: false, reason: "missing_header" };
4005
+ }
4006
+ const received = header.trim().toLowerCase();
4007
+ if (!/^[0-9a-f]+$/.test(received)) {
4008
+ return { valid: false, reason: "malformed_header" };
4009
+ }
4010
+ const expected = createHmac2("sha256", args.secret).update(args.payload).digest("hex");
4011
+ const receivedBuf = Buffer.from(received, "utf8");
4012
+ const expectedBuf = Buffer.from(expected, "utf8");
4013
+ if (receivedBuf.length !== expectedBuf.length) {
4014
+ return { valid: false, reason: "no_match" };
4015
+ }
4016
+ if (!timingSafeEqual4(receivedBuf, expectedBuf)) {
4017
+ return { valid: false, reason: "no_match" };
4018
+ }
4019
+ return { valid: true };
4020
+ }
4021
+
3737
4022
  // src/routes/billing.ts
3738
4023
  var BILLING_UNAVAILABLE = apiError(
3739
4024
  "internal_error",
@@ -3788,14 +4073,139 @@ function requireBillingPrincipal(req, res, next) {
3788
4073
  next();
3789
4074
  });
3790
4075
  }
3791
- var CheckoutSchema = z3.object({
3792
- billing_cycle: z3.enum(["monthly", "yearly"]).default("monthly")
4076
+ var CheckoutSchema = z2.object({
4077
+ // Was absent, and the route hardcoded PAID_PLAN_ID. The crypto lane sold
4078
+ // Starter and this one did not, so the two processors sold different
4079
+ // catalogues -- with Starter the emphasised middle card on /pricing.
4080
+ plan_id: z2.string().min(1).default(PAID_PLAN_ID),
4081
+ billing_cycle: z2.enum(["monthly", "yearly"]).default("monthly")
3793
4082
  });
3794
4083
  var OCCUPYING_BILLING_STATUSES = /* @__PURE__ */ new Set(["active", "past_due", "unpaid"]);
4084
+ var CHECKOUT_LOCK_TTL_MS = 6e4;
4085
+ var CHECKOUT_LOCK_PREFIX = "billing:checkout:";
4086
+ var localCheckoutLocks = /* @__PURE__ */ new Map();
4087
+ async function acquireCheckoutLock(userId) {
4088
+ const key = `${CHECKOUT_LOCK_PREFIX}${userId}`;
4089
+ const redis = isRedisAvailable() ? getRedisClient() : null;
4090
+ if (redis) {
4091
+ try {
4092
+ const claimed = await redis.set(
4093
+ key,
4094
+ "1",
4095
+ "PX",
4096
+ CHECKOUT_LOCK_TTL_MS,
4097
+ "NX"
4098
+ );
4099
+ return claimed === "OK";
4100
+ } catch (err) {
4101
+ secureLogger.warn("Checkout lock fell back to process memory", {
4102
+ reason: err instanceof Error ? err.message : "unknown"
4103
+ });
4104
+ }
4105
+ }
4106
+ const now = Date.now();
4107
+ const heldUntil = localCheckoutLocks.get(key);
4108
+ if (heldUntil !== void 0 && heldUntil > now) return false;
4109
+ localCheckoutLocks.set(key, now + CHECKOUT_LOCK_TTL_MS);
4110
+ return true;
4111
+ }
4112
+ async function releaseCheckoutLock(userId) {
4113
+ const key = `${CHECKOUT_LOCK_PREFIX}${userId}`;
4114
+ localCheckoutLocks.delete(key);
4115
+ const redis = isRedisAvailable() ? getRedisClient() : null;
4116
+ if (!redis) return;
4117
+ try {
4118
+ await redis.del(key);
4119
+ } catch {
4120
+ }
4121
+ }
4122
+ var CHECKOUT_RACE_LOST = apiError(
4123
+ "invalid_request_error",
4124
+ "CHECKOUT_IN_PROGRESS",
4125
+ "Another checkout request for this account is still being created. Next action: wait a couple of seconds and retry \u2014 two simultaneous requests can only ever produce one charge."
4126
+ );
4127
+ function checkoutAlreadyOpen(chargeId) {
4128
+ return apiError(
4129
+ "invalid_request_error",
4130
+ "CHECKOUT_IN_PROGRESS",
4131
+ `A payment page for this account is already open${chargeId ? ` (charge ${chargeId})` : ""}. Next action: finish that payment, or release the hold with POST /api/v1/billing/checkout/abandon if that page was abandoned \u2014 otherwise it clears on its own ${OPEN_CHARGE_WINDOW_MINUTES} minutes after the page was opened.`
4132
+ );
4133
+ }
4134
+ function withCheckoutLock(req, res, next) {
4135
+ const userId = req.billingUserId;
4136
+ if (!userId) {
4137
+ next();
4138
+ return;
4139
+ }
4140
+ void acquireCheckoutLock(userId).then((claimed) => {
4141
+ if (!claimed) {
4142
+ res.status(409).json(CHECKOUT_RACE_LOST);
4143
+ return;
4144
+ }
4145
+ const sendJson = res.json.bind(res);
4146
+ res.json = ((body) => {
4147
+ void releaseCheckoutLock(userId);
4148
+ return sendJson(body);
4149
+ });
4150
+ next();
4151
+ });
4152
+ }
4153
+ var OPEN_CHARGE_WINDOW_MINUTES = 15;
4154
+ var OPEN_CHARGE_WINDOW_MS = OPEN_CHARGE_WINDOW_MINUTES * 60 * 1e3;
4155
+ var ABANDONED_EVENT_TYPE = "checkout:abandoned";
4156
+ function openChargeCutoff() {
4157
+ return new Date(Date.now() - OPEN_CHARGE_WINDOW_MS).toISOString();
4158
+ }
4159
+ async function findOpenCharge(userId) {
4160
+ const { data, error } = await getSupabaseAdmin().from("billing_charges").select("charge_id").eq("user_id", userId).is("settled_at", null).neq("status", "canceled").gte("created_at", openChargeCutoff()).limit(1);
4161
+ if (error) {
4162
+ secureLogger.warn("Open-charge lookup failed; allowing checkout", {
4163
+ userId,
4164
+ errorMessage: error.message
4165
+ });
4166
+ return null;
4167
+ }
4168
+ const row = Array.isArray(data) ? data[0] : void 0;
4169
+ return typeof row?.charge_id === "string" ? row.charge_id : null;
4170
+ }
4171
+ async function settleChargeRecord(chargeId, status, eventType) {
4172
+ if (!chargeId || !isSupabaseConfigured()) return;
4173
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4174
+ try {
4175
+ const { error } = await getSupabaseAdmin().from("billing_charges").update({
4176
+ status,
4177
+ settled_at: now,
4178
+ last_event_at: now,
4179
+ last_event_type: eventType
4180
+ }).eq("charge_id", chargeId);
4181
+ if (error) throw new Error(error.message);
4182
+ } catch (err) {
4183
+ secureLogger.error("Failed to settle billing charge record", err, {
4184
+ chargeId,
4185
+ eventType
4186
+ });
4187
+ }
4188
+ }
4189
+ async function recordChargeCreated(charge) {
4190
+ const { error } = await getSupabaseAdmin().from("billing_charges").insert({
4191
+ charge_id: charge.chargeId,
4192
+ processor: charge.processor,
4193
+ user_id: charge.userId,
4194
+ plan_id: charge.planId,
4195
+ billing_cycle: charge.cycle,
4196
+ amount_usd: charge.amountUsd,
4197
+ plan_id_before: charge.planIdBefore,
4198
+ status: "created"
4199
+ });
4200
+ if (error) {
4201
+ throw new Error(`failed to record billing charge: ${error.message}`);
4202
+ }
4203
+ }
3795
4204
  var billingRouter = Router6();
3796
4205
  billingRouter.post(
3797
4206
  "/checkout-session",
3798
4207
  requireBillingPrincipal,
4208
+ withCheckoutLock,
3799
4209
  asyncHandler(async (req, res) => {
3800
4210
  const userId = req.billingUserId;
3801
4211
  if (!isStripeCheckoutConfigured()) {
@@ -3814,11 +4224,23 @@ billingRouter.post(
3814
4224
  )
3815
4225
  );
3816
4226
  }
3817
- const priceId = priceIdForCycle(parsed.data.billing_cycle);
3818
- if (!priceId) {
4227
+ const { plan_id: planId, billing_cycle: cycle } = parsed.data;
4228
+ if (!isSellablePlan(planId)) {
4229
+ return res.status(400).json(
4230
+ apiError(
4231
+ "invalid_request_error",
4232
+ "PLAN_NOT_SELLABLE",
4233
+ `Plan "${planId}" is not sold self-serve. Next action: choose starter or ${PAID_PLAN_ID}, or contact support for Enterprise.`
4234
+ )
4235
+ );
4236
+ }
4237
+ const priceId = priceIdForPlanCycle(planId, cycle);
4238
+ if (!priceId) {
3819
4239
  return res.status(503).json(BILLING_UNAVAILABLE);
3820
4240
  }
3821
- const { data: profile } = await getSupabaseAdmin().from("user_profiles").select("billing_customer_id, billing_subscription_id, billing_status").eq("id", userId).maybeSingle();
4241
+ const { data: profile } = await getSupabaseAdmin().from("user_profiles").select(
4242
+ "plan_id, billing_customer_id, billing_subscription_id, billing_status"
4243
+ ).eq("id", userId).maybeSingle();
3822
4244
  if (profile?.billing_subscription_id && typeof profile.billing_status === "string" && OCCUPYING_BILLING_STATUSES.has(profile.billing_status)) {
3823
4245
  return res.status(409).json(
3824
4246
  apiError(
@@ -3828,6 +4250,10 @@ billingRouter.post(
3828
4250
  )
3829
4251
  );
3830
4252
  }
4253
+ const openChargeId = await findOpenCharge(userId);
4254
+ if (openChargeId) {
4255
+ return res.status(409).json(checkoutAlreadyOpen(openChargeId));
4256
+ }
3831
4257
  try {
3832
4258
  let customerId = profile?.billing_customer_id ?? null;
3833
4259
  if (!customerId) {
@@ -3846,11 +4272,25 @@ billingRouter.post(
3846
4272
  customerId,
3847
4273
  priceId,
3848
4274
  userId,
3849
- planId: PAID_PLAN_ID
4275
+ planId
4276
+ });
4277
+ await recordChargeCreated({
4278
+ chargeId: session.id,
4279
+ processor: "stripe",
4280
+ userId,
4281
+ planId,
4282
+ cycle,
4283
+ amountUsd: priceForPlanCycle(planId, cycle) ?? 0,
4284
+ planIdBefore: asString(profile?.plan_id)
3850
4285
  });
3851
4286
  return res.json({
3852
4287
  ok: true,
3853
- data: { url: session.url, session_id: session.id }
4288
+ data: {
4289
+ url: session.url,
4290
+ session_id: session.id,
4291
+ plan_id: planId,
4292
+ billing_cycle: cycle
4293
+ }
3854
4294
  });
3855
4295
  } catch (err) {
3856
4296
  if (err instanceof StripeApiError) {
@@ -3922,176 +4362,566 @@ function invoiceSubscriptionContext(invoice) {
3922
4362
  ...metadataOf(parentDetails.metadata),
3923
4363
  ...metadataOf(details.metadata)
3924
4364
  }
3925
- };
3926
- }
3927
- function invoicePeriod(invoice) {
3928
- const lines = metadataOf(invoice.lines);
3929
- const data = Array.isArray(lines.data) ? lines.data : [];
3930
- let best = null;
3931
- let bestEnd = -Infinity;
3932
- for (const raw of data) {
3933
- const line = metadataOf(raw);
3934
- const period2 = metadataOf(line.period);
3935
- const end = typeof period2.end === "number" ? period2.end : null;
3936
- if (end !== null && end > bestEnd) {
3937
- bestEnd = end;
3938
- best = line;
4365
+ };
4366
+ }
4367
+ function invoicePeriod(invoice) {
4368
+ const lines = metadataOf(invoice.lines);
4369
+ const data = Array.isArray(lines.data) ? lines.data : [];
4370
+ let best = null;
4371
+ let bestEnd = -Infinity;
4372
+ for (const raw of data) {
4373
+ const line = metadataOf(raw);
4374
+ const period2 = metadataOf(line.period);
4375
+ const end = typeof period2.end === "number" ? period2.end : null;
4376
+ if (end !== null && end > bestEnd) {
4377
+ bestEnd = end;
4378
+ best = line;
4379
+ }
4380
+ }
4381
+ if (!best) return { cycleStart: null, cycleEnd: null, priceId: null };
4382
+ const period = metadataOf(best.period);
4383
+ const price = metadataOf(best.price);
4384
+ const pricing = metadataOf(best.pricing);
4385
+ const priceDetails = metadataOf(pricing.price_details);
4386
+ return {
4387
+ cycleStart: isoFromUnixSeconds(period.start),
4388
+ cycleEnd: isoFromUnixSeconds(period.end),
4389
+ priceId: asString(price.id) ?? asString(priceDetails.price)
4390
+ };
4391
+ }
4392
+ function cappedCreditCycleEnd(cycleStart, cycleEnd) {
4393
+ if (!cycleStart) return cycleEnd;
4394
+ const start = new Date(cycleStart);
4395
+ if (Number.isNaN(start.getTime())) return cycleEnd;
4396
+ const monthly = creditCycleEndFrom(start);
4397
+ if (!cycleEnd) return monthly.toISOString();
4398
+ const end = new Date(cycleEnd);
4399
+ if (Number.isNaN(end.getTime())) return monthly.toISOString();
4400
+ return (end < monthly ? end : monthly).toISOString();
4401
+ }
4402
+ async function decideWebhookAction(event) {
4403
+ const object = metadataOf(event.data?.object);
4404
+ switch (event.type) {
4405
+ case "checkout.session.completed": {
4406
+ const mode = asString(object.mode);
4407
+ if (mode !== null && mode !== "subscription") {
4408
+ return { kind: "ignore", note: "non-subscription checkout" };
4409
+ }
4410
+ const metadata = metadataOf(object.metadata);
4411
+ const userId = asString(metadata.user_id) ?? asString(object.client_reference_id);
4412
+ if (!userId) {
4413
+ return { kind: "ignore", note: "checkout session without user_id" };
4414
+ }
4415
+ return {
4416
+ kind: "apply",
4417
+ args: {
4418
+ eventId: event.id,
4419
+ eventType: event.type,
4420
+ userId,
4421
+ newPlanId: asString(metadata.plan_id) ?? PAID_PLAN_ID,
4422
+ grantCredits: true,
4423
+ billingStatus: "active",
4424
+ customerId: refId(object.customer),
4425
+ subscriptionId: refId(object.subscription),
4426
+ subscriptionStatus: "active",
4427
+ ledgerReason: "plan_change"
4428
+ }
4429
+ };
4430
+ }
4431
+ case "customer.subscription.updated":
4432
+ case "customer.subscription.deleted": {
4433
+ const metadata = metadataOf(object.metadata);
4434
+ const customerId = refId(object.customer);
4435
+ const userId = asString(metadata.user_id) ?? await resolveUserIdByCustomer(customerId);
4436
+ if (!userId) {
4437
+ return {
4438
+ kind: "unresolved_user",
4439
+ note: `no user binding for customer ${customerId ?? "<none>"}`
4440
+ };
4441
+ }
4442
+ if (event.type === "customer.subscription.deleted") {
4443
+ return {
4444
+ kind: "apply",
4445
+ args: {
4446
+ eventId: event.id,
4447
+ eventType: event.type,
4448
+ userId,
4449
+ newPlanId: FREE_PLAN_ID,
4450
+ grantCredits: true,
4451
+ billingStatus: "canceled",
4452
+ customerId,
4453
+ subscriptionId: refId(object.id),
4454
+ subscriptionStatus: "canceled",
4455
+ ledgerReason: "plan_change"
4456
+ }
4457
+ };
4458
+ }
4459
+ const status = asString(object.status);
4460
+ const items = metadataOf(object.items);
4461
+ const firstItem = metadataOf(
4462
+ Array.isArray(items.data) ? items.data[0] : void 0
4463
+ );
4464
+ const priceId = asString(metadataOf(firstItem.price).id);
4465
+ return {
4466
+ kind: "apply",
4467
+ args: {
4468
+ eventId: event.id,
4469
+ eventType: event.type,
4470
+ userId,
4471
+ // Tier/status sync only — credits ride the money event
4472
+ // (invoice.paid), which is the one wallet clock.
4473
+ newPlanId: planIdForPrice(priceId) ?? asString(metadata.plan_id) ?? null,
4474
+ grantCredits: false,
4475
+ billingStatus: mapSubscriptionStatusToBilling(status),
4476
+ customerId,
4477
+ subscriptionId: refId(object.id),
4478
+ // Whitelisted, never forwarded raw. stripe_subscriptions.status has
4479
+ // a CHECK over eight literals; a status Stripe adds later raises
4480
+ // 23514 *inside* apply_plan_change, which rolls back the dedupe
4481
+ // claim and the wallet grant with it, 500s, and leaves Stripe
4482
+ // retrying forever. Null here means "skip the registry upsert",
4483
+ // which still applies the tier and still acks 200.
4484
+ subscriptionStatus: registrySubscriptionStatus(status),
4485
+ priceId,
4486
+ ledgerReason: "plan_change"
4487
+ }
4488
+ };
4489
+ }
4490
+ case "invoice.paid":
4491
+ case "invoice.payment_failed": {
4492
+ const { subscriptionId, metadata } = invoiceSubscriptionContext(object);
4493
+ if (!subscriptionId) {
4494
+ return { kind: "ignore", note: "invoice without a subscription" };
4495
+ }
4496
+ const customerId = refId(object.customer);
4497
+ const userId = asString(metadata.user_id) ?? await resolveUserIdByCustomer(customerId);
4498
+ if (!userId) {
4499
+ return {
4500
+ kind: "unresolved_user",
4501
+ note: `no user binding for customer ${customerId ?? "<none>"}`
4502
+ };
4503
+ }
4504
+ if (event.type === "invoice.payment_failed") {
4505
+ return {
4506
+ kind: "apply",
4507
+ args: {
4508
+ eventId: event.id,
4509
+ eventType: event.type,
4510
+ userId,
4511
+ billingStatus: "past_due",
4512
+ customerId,
4513
+ subscriptionId,
4514
+ ledgerReason: "plan_change"
4515
+ }
4516
+ };
4517
+ }
4518
+ const period = invoicePeriod(object);
4519
+ const creditCycleEnd = cappedCreditCycleEnd(
4520
+ period.cycleStart,
4521
+ period.cycleEnd
4522
+ );
4523
+ return {
4524
+ kind: "apply",
4525
+ args: {
4526
+ eventId: event.id,
4527
+ eventType: event.type,
4528
+ userId,
4529
+ newPlanId: planIdForPrice(period.priceId) ?? asString(metadata.plan_id) ?? null,
4530
+ grantCredits: true,
4531
+ cycleStart: period.cycleStart,
4532
+ cycleEnd: creditCycleEnd,
4533
+ termEnd: period.cycleEnd,
4534
+ billingStatus: "active",
4535
+ customerId,
4536
+ subscriptionId,
4537
+ priceId: period.priceId,
4538
+ ledgerReason: "refill"
4539
+ }
4540
+ };
4541
+ }
4542
+ default:
4543
+ return { kind: "ignore", note: `unhandled event type ${event.type}` };
4544
+ }
4545
+ }
4546
+ var stripeWebhookHandler = asyncHandler(
4547
+ async (req, res) => {
4548
+ const secret = getStripeWebhookSecret();
4549
+ if (!secret) {
4550
+ return res.status(503).json(BILLING_UNAVAILABLE);
4551
+ }
4552
+ const raw = req.body;
4553
+ if (!Buffer.isBuffer(raw)) {
4554
+ secureLogger.error(
4555
+ "Stripe webhook received a non-raw body \u2014 express.raw must mount before the JSON parser",
4556
+ void 0,
4557
+ { path: req.path }
4558
+ );
4559
+ return res.status(500).json(
4560
+ apiError(
4561
+ "internal_error",
4562
+ "WEBHOOK_BODY_NOT_RAW",
4563
+ "Webhook raw body unavailable; server middleware misconfigured."
4564
+ )
4565
+ );
4566
+ }
4567
+ const verification = verifyStripeSignature({
4568
+ payload: raw,
4569
+ header: req.headers["stripe-signature"],
4570
+ secret
4571
+ });
4572
+ if (!verification.valid) {
4573
+ secureLogger.warn("Stripe webhook signature rejected", {
4574
+ reason: verification.reason
4575
+ });
4576
+ return res.status(400).json(
4577
+ apiError(
4578
+ "authentication_error",
4579
+ "INVALID_WEBHOOK_SIGNATURE",
4580
+ `Stripe signature verification failed (${verification.reason}).`
4581
+ )
4582
+ );
4583
+ }
4584
+ let event;
4585
+ try {
4586
+ event = JSON.parse(raw.toString("utf8"));
4587
+ } catch {
4588
+ return res.status(400).json(
4589
+ apiError(
4590
+ "invalid_request_error",
4591
+ "INVALID_JSON",
4592
+ "Webhook payload is not valid JSON."
4593
+ )
4594
+ );
4595
+ }
4596
+ if (!asString(event?.id) || !asString(event?.type)) {
4597
+ return res.status(400).json(
4598
+ apiError(
4599
+ "invalid_request_error",
4600
+ "INVALID_EVENT",
4601
+ "Webhook payload is missing event id or type."
4602
+ )
4603
+ );
4604
+ }
4605
+ try {
4606
+ const decision = await decideWebhookAction(event);
4607
+ if (decision.kind === "ignore") {
4608
+ return res.json({ received: true, handled: false });
4609
+ }
4610
+ if (decision.kind === "unresolved_user") {
4611
+ secureLogger.warn("Stripe webhook user unresolved \u2014 asking for retry", {
4612
+ eventType: event.type,
4613
+ note: decision.note
4614
+ });
4615
+ return res.status(500).json(
4616
+ apiError(
4617
+ "internal_error",
4618
+ "WEBHOOK_USER_UNRESOLVED",
4619
+ "Could not resolve the Hive user for this event yet; Stripe should retry."
4620
+ )
4621
+ );
4622
+ }
4623
+ const result = await applyPlanChange(decision.args);
4624
+ if (event.type === "checkout.session.completed") {
4625
+ await settleChargeRecord(
4626
+ asString(metadataOf(event.data?.object).id),
4627
+ "confirmed",
4628
+ event.type
4629
+ );
4630
+ }
4631
+ return res.json({
4632
+ received: true,
4633
+ handled: true,
4634
+ outcome: result.outcome,
4635
+ replayed: result.replayed
4636
+ });
4637
+ } catch (err) {
4638
+ secureLogger.error("Stripe webhook processing failed", err, {
4639
+ eventType: event.type
4640
+ });
4641
+ return res.status(500).json(
4642
+ apiError(
4643
+ "internal_error",
4644
+ "WEBHOOK_PROCESSING_FAILED",
4645
+ "Webhook processing failed; Stripe should retry. Processing is idempotent by event id."
4646
+ )
4647
+ );
4648
+ }
4649
+ }
4650
+ );
4651
+ var LLAMAPAY_UNAVAILABLE = apiError(
4652
+ "internal_error",
4653
+ "BILLING_UNAVAILABLE",
4654
+ "Crypto checkout is not configured on this deployment. Next action: contact support@hiveintelligence.xyz to upgrade, or retry once billing is enabled."
4655
+ );
4656
+ var CryptoCheckoutSchema = z2.object({
4657
+ plan_id: z2.string().min(1).default(PAID_PLAN_ID),
4658
+ billing_cycle: z2.enum(["monthly", "yearly"]).default("monthly")
4659
+ });
4660
+ billingRouter.post(
4661
+ "/checkout",
4662
+ requireBillingPrincipal,
4663
+ withCheckoutLock,
4664
+ asyncHandler(async (req, res) => {
4665
+ const userId = req.billingUserId;
4666
+ if (!isLlamaPayCheckoutConfigured() || !isSupabaseConfigured()) {
4667
+ return res.status(503).json(LLAMAPAY_UNAVAILABLE);
4668
+ }
4669
+ const parsed = CryptoCheckoutSchema.safeParse(req.body ?? {});
4670
+ if (!parsed.success) {
4671
+ return res.status(400).json(
4672
+ apiError(
4673
+ "invalid_request_error",
4674
+ "VALIDATION_ERROR",
4675
+ 'billing_cycle must be "monthly" or "yearly", and plan_id must be a sold plan.'
4676
+ )
4677
+ );
4678
+ }
4679
+ const { plan_id: planId, billing_cycle: cycle } = parsed.data;
4680
+ if (!isSellablePlan(planId)) {
4681
+ return res.status(400).json(
4682
+ apiError(
4683
+ "invalid_request_error",
4684
+ "PLAN_NOT_SELLABLE",
4685
+ `Plan "${planId}" is not sold self-serve. Next action: choose starter or ${PAID_PLAN_ID}, or contact support for Enterprise.`
4686
+ )
4687
+ );
4688
+ }
4689
+ const amountUsd = priceForPlanCycle(planId, cycle);
4690
+ if (amountUsd === void 0) {
4691
+ return res.status(503).json(LLAMAPAY_UNAVAILABLE);
4692
+ }
4693
+ const { data: profile } = await getSupabaseAdmin().from("user_profiles").select("plan_id, billing_subscription_id, billing_status").eq("id", userId).maybeSingle();
4694
+ const hasOccupyingSubscription = Boolean(profile?.billing_subscription_id) && typeof profile?.billing_status === "string" && OCCUPYING_BILLING_STATUSES.has(profile.billing_status);
4695
+ const { data: liveSubscription } = await getSupabaseAdmin().from("stripe_subscriptions").select("current_period_end").eq("user_id", userId).in("status", [...OCCUPYING_BILLING_STATUSES]).order("current_period_end", { ascending: false }).limit(1).maybeSingle();
4696
+ if (!canPurchasePlan(
4697
+ profile?.plan_id ?? null,
4698
+ planId,
4699
+ liveSubscription?.current_period_end ?? null
4700
+ )) {
4701
+ const sameTier = profile?.plan_id === planId;
4702
+ return res.status(409).json(
4703
+ apiError(
4704
+ "invalid_request_error",
4705
+ hasOccupyingSubscription ? "SUBSCRIPTION_EXISTS" : "PLAN_NOT_AN_UPGRADE",
4706
+ sameTier ? `This account is already on ${planId} and the current term still has more than ${RENEWAL_WINDOW_DAYS} days left, so renewing now would replace time you have already paid for. Next action: renew inside the last ${RENEWAL_WINDOW_DAYS} days, pick a higher tier to upgrade immediately, or message us at https://t.me/HiveIntelligence.` : `This account is already on ${profile?.plan_id ?? "a paid plan"}. Next action: pick a higher tier to upgrade now, or message us at https://t.me/HiveIntelligence to change or cancel the current plan.`
4707
+ )
4708
+ );
4709
+ }
4710
+ const openChargeId = await findOpenCharge(userId);
4711
+ if (openChargeId) {
4712
+ return res.status(409).json(checkoutAlreadyOpen(openChargeId));
4713
+ }
4714
+ try {
4715
+ const charge = await createLlamaPayCharge({
4716
+ userId,
4717
+ planId,
4718
+ cycle,
4719
+ amountUsd,
4720
+ email: req.billingUserEmail
4721
+ });
4722
+ await recordChargeCreated({
4723
+ chargeId: charge.id,
4724
+ processor: "llamapay",
4725
+ userId,
4726
+ planId,
4727
+ cycle,
4728
+ amountUsd,
4729
+ planIdBefore: asString(profile?.plan_id)
4730
+ });
4731
+ return res.json({
4732
+ ok: true,
4733
+ data: {
4734
+ url: charge.hostedUrl,
4735
+ charge_id: charge.id,
4736
+ plan_id: planId,
4737
+ billing_cycle: cycle,
4738
+ amount_usd: amountUsd,
4739
+ processor: "llamapay"
4740
+ }
4741
+ });
4742
+ } catch (err) {
4743
+ if (err instanceof LlamaPayApiError) {
4744
+ secureLogger.error("LlamaPay charge creation failed", err, {
4745
+ userId,
4746
+ status: err.status
4747
+ });
4748
+ return res.status(502).json(
4749
+ apiError(
4750
+ "provider_error",
4751
+ "BILLING_PROVIDER_ERROR",
4752
+ "LlamaPay rejected the checkout request. Next action: retry; if it persists, contact support@hiveintelligence.xyz."
4753
+ )
4754
+ );
4755
+ }
4756
+ throw err;
4757
+ }
4758
+ })
4759
+ );
4760
+ var AbandonSchema = z2.object({
4761
+ charge_id: z2.string().min(1).optional()
4762
+ });
4763
+ billingRouter.post(
4764
+ "/checkout/abandon",
4765
+ requireBillingPrincipal,
4766
+ asyncHandler(async (req, res) => {
4767
+ const userId = req.billingUserId;
4768
+ if (!isSupabaseConfigured()) {
4769
+ return res.status(503).json(BILLING_UNAVAILABLE);
4770
+ }
4771
+ const parsed = AbandonSchema.safeParse(req.body ?? {});
4772
+ if (!parsed.success) {
4773
+ return res.status(400).json(
4774
+ apiError(
4775
+ "invalid_request_error",
4776
+ "VALIDATION_ERROR",
4777
+ "charge_id must be a non-empty string when provided. Next action: omit it to release every checkout this account has open."
4778
+ )
4779
+ );
4780
+ }
4781
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4782
+ let query = getSupabaseAdmin().from("billing_charges").update({
4783
+ status: "canceled",
4784
+ last_event_at: now,
4785
+ last_event_type: ABANDONED_EVENT_TYPE
4786
+ }).eq("user_id", userId).eq("status", "created").is("settled_at", null).gte("created_at", openChargeCutoff());
4787
+ if (parsed.data.charge_id) {
4788
+ query = query.eq("charge_id", parsed.data.charge_id);
4789
+ }
4790
+ const { data, error } = await query.select("charge_id");
4791
+ if (error) {
4792
+ secureLogger.error("Failed to mark checkout abandoned", error, {
4793
+ userId
4794
+ });
4795
+ return res.status(500).json(
4796
+ apiError(
4797
+ "internal_error",
4798
+ "CHECKOUT_ABANDON_FAILED",
4799
+ `Could not release the open checkout. Next action: retry, or wait ${OPEN_CHARGE_WINDOW_MINUTES} minutes for it to clear on its own.`
4800
+ )
4801
+ );
4802
+ }
4803
+ const abandoned = (Array.isArray(data) ? data : []).map((row) => row.charge_id).filter((id) => typeof id === "string");
4804
+ return res.json({ ok: true, data: { abandoned } });
4805
+ })
4806
+ );
4807
+ billingRouter.get(
4808
+ "/charges",
4809
+ requireBillingPrincipal,
4810
+ asyncHandler(async (req, res) => {
4811
+ const userId = req.billingUserId;
4812
+ if (!isSupabaseConfigured()) {
4813
+ return res.status(503).json(BILLING_UNAVAILABLE);
4814
+ }
4815
+ const { data, error } = await getSupabaseAdmin().from("billing_charges").select(
4816
+ "charge_id, processor, plan_id, billing_cycle, amount_usd, status, created_at, settled_at"
4817
+ ).eq("user_id", userId).order("created_at", { ascending: false }).limit(50);
4818
+ if (error) {
4819
+ secureLogger.error("Failed to read billing charges", error, { userId });
4820
+ return res.status(500).json(
4821
+ apiError(
4822
+ "internal_error",
4823
+ "BILLING_HISTORY_UNAVAILABLE",
4824
+ "Could not load your billing history. Next action: retry shortly, or contact support@hiveintelligence.xyz with the date of the charge."
4825
+ )
4826
+ );
3939
4827
  }
4828
+ return res.json({ ok: true, data: { charges: data ?? [] } });
4829
+ })
4830
+ );
4831
+ var LLAMAPAY_EVENT_TYPES = [
4832
+ "charge:pending",
4833
+ "charge:confirmed",
4834
+ "subscription:expired"
4835
+ ];
4836
+ function localChargeAmountUsd(pricing) {
4837
+ const local = metadataOf(metadataOf(pricing).local);
4838
+ const currency = asString(local.currency);
4839
+ if (currency !== null && currency.toUpperCase() !== "USD") {
4840
+ return Number.NaN;
4841
+ }
4842
+ const raw = local.amount;
4843
+ const amount = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseFloat(raw) : void 0;
4844
+ if (amount === void 0 || !Number.isFinite(amount)) return void 0;
4845
+ return amount;
4846
+ }
4847
+ function decideLlamaPayAction(event) {
4848
+ const data = metadataOf(event.data);
4849
+ const metadata = metadataOf(data.metadata);
4850
+ const userId = asString(metadata.user_id);
4851
+ const chargeId = asString(data.id);
4852
+ if (!userId) {
4853
+ return { kind: "ignore", note: "charge without user_id metadata" };
3940
4854
  }
3941
- if (!best) return { cycleStart: null, cycleEnd: null, priceId: null };
3942
- const period = metadataOf(best.period);
3943
- const price = metadataOf(best.price);
3944
- const pricing = metadataOf(best.pricing);
3945
- const priceDetails = metadataOf(pricing.price_details);
3946
- return {
3947
- cycleStart: isoFromUnixSeconds(period.start),
3948
- cycleEnd: isoFromUnixSeconds(period.end),
3949
- priceId: asString(price.id) ?? asString(priceDetails.price)
3950
- };
3951
- }
3952
- async function decideWebhookAction(event) {
3953
- const object = metadataOf(event.data?.object);
3954
4855
  switch (event.type) {
3955
- case "checkout.session.completed": {
3956
- const mode = asString(object.mode);
3957
- if (mode !== null && mode !== "subscription") {
3958
- return { kind: "ignore", note: "non-subscription checkout" };
3959
- }
3960
- const metadata = metadataOf(object.metadata);
3961
- const userId = asString(metadata.user_id) ?? asString(object.client_reference_id);
3962
- if (!userId) {
3963
- return { kind: "ignore", note: "checkout session without user_id" };
4856
+ case "charge:pending":
4857
+ case "charge:confirmed": {
4858
+ const planId = asString(metadata.plan_id) ?? PAID_PLAN_ID;
4859
+ const cycle = metadata.billing_cycle === "yearly" ? "yearly" : "monthly";
4860
+ const expected = priceForPlanCycle(planId, cycle);
4861
+ const paid = localChargeAmountUsd(data.pricing);
4862
+ if (expected !== void 0 && paid !== void 0 && paid !== expected) {
4863
+ return {
4864
+ kind: "ignore",
4865
+ note: `charge ${chargeId ?? "<no id>"} paid ${Number.isNaN(paid) ? "a non-USD amount" : paid} against ${expected} for ${planId}/${cycle} \u2014 entitlement withheld`
4866
+ };
3964
4867
  }
4868
+ const start = event.created_at ? new Date(event.created_at) : /* @__PURE__ */ new Date();
4869
+ const cycleStart = Number.isNaN(start.getTime()) ? /* @__PURE__ */ new Date() : start;
3965
4870
  return {
3966
4871
  kind: "apply",
3967
4872
  args: {
3968
4873
  eventId: event.id,
3969
4874
  eventType: event.type,
3970
4875
  userId,
3971
- newPlanId: asString(metadata.plan_id) ?? PAID_PLAN_ID,
4876
+ newPlanId: planId,
3972
4877
  grantCredits: true,
4878
+ cycleStart: cycleStart.toISOString(),
4879
+ // Two clocks, deliberately. Credits are sold monthly on every term,
4880
+ // so the wallet cycle is always a month; the term is what the charge
4881
+ // actually bought. Passing the annual date as the credit cycle gave
4882
+ // a $1,490 buyer one month's credits to last a year.
4883
+ cycleEnd: creditCycleEndFrom(cycleStart).toISOString(),
4884
+ termEnd: termEndFrom(cycleStart, cycle).toISOString(),
3973
4885
  billingStatus: "active",
3974
- customerId: refId(object.customer),
3975
- subscriptionId: refId(object.subscription),
4886
+ subscriptionId: chargeId,
3976
4887
  subscriptionStatus: "active",
3977
4888
  ledgerReason: "plan_change"
3978
4889
  }
3979
4890
  };
3980
4891
  }
3981
- case "customer.subscription.updated":
3982
- case "customer.subscription.deleted": {
3983
- const metadata = metadataOf(object.metadata);
3984
- const customerId = refId(object.customer);
3985
- const userId = asString(metadata.user_id) ?? await resolveUserIdByCustomer(customerId);
3986
- if (!userId) {
3987
- return {
3988
- kind: "unresolved_user",
3989
- note: `no user binding for customer ${customerId ?? "<none>"}`
3990
- };
3991
- }
3992
- if (event.type === "customer.subscription.deleted") {
3993
- return {
3994
- kind: "apply",
3995
- args: {
3996
- eventId: event.id,
3997
- eventType: event.type,
3998
- userId,
3999
- newPlanId: FREE_PLAN_ID,
4000
- grantCredits: true,
4001
- billingStatus: "canceled",
4002
- customerId,
4003
- subscriptionId: refId(object.id),
4004
- subscriptionStatus: "canceled",
4005
- ledgerReason: "plan_change"
4006
- }
4007
- };
4008
- }
4009
- const status = asString(object.status);
4010
- const items = metadataOf(object.items);
4011
- const firstItem = metadataOf(
4012
- Array.isArray(items.data) ? items.data[0] : void 0
4013
- );
4014
- const priceId = asString(metadataOf(firstItem.price).id);
4892
+ case "subscription:expired": {
4015
4893
  return {
4016
4894
  kind: "apply",
4017
4895
  args: {
4018
4896
  eventId: event.id,
4019
4897
  eventType: event.type,
4020
4898
  userId,
4021
- // Tier/status sync only — credits ride the money event
4022
- // (invoice.paid), which is the one wallet clock.
4023
- newPlanId: planIdForPrice(priceId) ?? asString(metadata.plan_id) ?? null,
4024
- grantCredits: false,
4025
- billingStatus: mapSubscriptionStatusToBilling(status),
4026
- customerId,
4027
- subscriptionId: refId(object.id),
4028
- subscriptionStatus: status,
4029
- priceId,
4899
+ newPlanId: FREE_PLAN_ID,
4900
+ grantCredits: true,
4901
+ billingStatus: "canceled",
4902
+ subscriptionId: chargeId,
4903
+ subscriptionStatus: "canceled",
4030
4904
  ledgerReason: "plan_change"
4031
4905
  }
4032
4906
  };
4033
4907
  }
4034
- case "invoice.paid":
4035
- case "invoice.payment_failed": {
4036
- const { subscriptionId, metadata } = invoiceSubscriptionContext(object);
4037
- if (!subscriptionId) {
4038
- return { kind: "ignore", note: "invoice without a subscription" };
4039
- }
4040
- const customerId = refId(object.customer);
4041
- const userId = asString(metadata.user_id) ?? await resolveUserIdByCustomer(customerId);
4042
- if (!userId) {
4043
- return {
4044
- kind: "unresolved_user",
4045
- note: `no user binding for customer ${customerId ?? "<none>"}`
4046
- };
4047
- }
4048
- if (event.type === "invoice.payment_failed") {
4049
- return {
4050
- kind: "apply",
4051
- args: {
4052
- eventId: event.id,
4053
- eventType: event.type,
4054
- userId,
4055
- billingStatus: "past_due",
4056
- customerId,
4057
- subscriptionId,
4058
- ledgerReason: "plan_change"
4059
- }
4060
- };
4061
- }
4062
- const period = invoicePeriod(object);
4908
+ default:
4063
4909
  return {
4064
- kind: "apply",
4065
- args: {
4066
- eventId: event.id,
4067
- eventType: event.type,
4068
- userId,
4069
- newPlanId: planIdForPrice(period.priceId) ?? asString(metadata.plan_id) ?? null,
4070
- grantCredits: true,
4071
- cycleStart: period.cycleStart,
4072
- cycleEnd: period.cycleEnd,
4073
- billingStatus: "active",
4074
- customerId,
4075
- subscriptionId,
4076
- priceId: period.priceId,
4077
- ledgerReason: "refill"
4078
- }
4910
+ kind: "ignore",
4911
+ note: `unhandled event type ${event.type} (known: ${LLAMAPAY_EVENT_TYPES.join(", ")})`
4079
4912
  };
4080
- }
4081
- default:
4082
- return { kind: "ignore", note: `unhandled event type ${event.type}` };
4083
4913
  }
4084
4914
  }
4085
- var stripeWebhookHandler = asyncHandler(
4915
+ var llamapayWebhookHandler = asyncHandler(
4086
4916
  async (req, res) => {
4087
- const secret = getStripeWebhookSecret();
4917
+ const secret = getLlamaPayWebhookSecret();
4088
4918
  if (!secret) {
4089
- return res.status(503).json(BILLING_UNAVAILABLE);
4919
+ return res.status(503).json(LLAMAPAY_UNAVAILABLE);
4090
4920
  }
4091
4921
  const raw = req.body;
4092
4922
  if (!Buffer.isBuffer(raw)) {
4093
4923
  secureLogger.error(
4094
- "Stripe webhook received a non-raw body \u2014 express.raw must mount before the JSON parser",
4924
+ "LlamaPay webhook received a non-raw body \u2014 express.raw must mount before the JSON parser",
4095
4925
  void 0,
4096
4926
  { path: req.path }
4097
4927
  );
@@ -4103,26 +4933,26 @@ var stripeWebhookHandler = asyncHandler(
4103
4933
  )
4104
4934
  );
4105
4935
  }
4106
- const verification = verifyStripeSignature({
4936
+ const verification = verifyLlamaPaySignature({
4107
4937
  payload: raw,
4108
- header: req.headers["stripe-signature"],
4938
+ header: req.headers[LLAMAPAY_SIGNATURE_HEADER],
4109
4939
  secret
4110
4940
  });
4111
4941
  if (!verification.valid) {
4112
- secureLogger.warn("Stripe webhook signature rejected", {
4942
+ secureLogger.warn("LlamaPay webhook signature rejected", {
4113
4943
  reason: verification.reason
4114
4944
  });
4115
4945
  return res.status(400).json(
4116
4946
  apiError(
4117
4947
  "authentication_error",
4118
4948
  "INVALID_WEBHOOK_SIGNATURE",
4119
- `Stripe signature verification failed (${verification.reason}).`
4949
+ `LlamaPay signature verification failed (${verification.reason}).`
4120
4950
  )
4121
4951
  );
4122
4952
  }
4123
- let event;
4953
+ let envelope;
4124
4954
  try {
4125
- event = JSON.parse(raw.toString("utf8"));
4955
+ envelope = JSON.parse(raw.toString("utf8"));
4126
4956
  } catch {
4127
4957
  return res.status(400).json(
4128
4958
  apiError(
@@ -4132,6 +4962,7 @@ var stripeWebhookHandler = asyncHandler(
4132
4962
  )
4133
4963
  );
4134
4964
  }
4965
+ const event = envelope.event ?? envelope;
4135
4966
  if (!asString(event?.id) || !asString(event?.type)) {
4136
4967
  return res.status(400).json(
4137
4968
  apiError(
@@ -4142,20 +4973,20 @@ var stripeWebhookHandler = asyncHandler(
4142
4973
  );
4143
4974
  }
4144
4975
  try {
4145
- const decision = await decideWebhookAction(event);
4976
+ const decision = decideLlamaPayAction(event);
4146
4977
  if (decision.kind === "ignore") {
4147
- return res.json({ received: true, handled: false });
4148
- }
4149
- if (decision.kind === "unresolved_user") {
4150
- secureLogger.warn("Stripe webhook user unresolved \u2014 asking for retry", {
4978
+ secureLogger.warn("LlamaPay webhook ignored", {
4151
4979
  eventType: event.type,
4152
4980
  note: decision.note
4153
4981
  });
4982
+ return res.json({ received: true, handled: false });
4983
+ }
4984
+ if (decision.kind === "unresolved_user") {
4154
4985
  return res.status(500).json(
4155
4986
  apiError(
4156
4987
  "internal_error",
4157
4988
  "WEBHOOK_USER_UNRESOLVED",
4158
- "Could not resolve the Hive user for this event yet; Stripe should retry."
4989
+ "Could not resolve the Hive user for this event yet; LlamaPay should retry."
4159
4990
  )
4160
4991
  );
4161
4992
  }
@@ -4167,14 +4998,14 @@ var stripeWebhookHandler = asyncHandler(
4167
4998
  replayed: result.replayed
4168
4999
  });
4169
5000
  } catch (err) {
4170
- secureLogger.error("Stripe webhook processing failed", err, {
5001
+ secureLogger.error("LlamaPay webhook processing failed", err, {
4171
5002
  eventType: event.type
4172
5003
  });
4173
5004
  return res.status(500).json(
4174
5005
  apiError(
4175
5006
  "internal_error",
4176
5007
  "WEBHOOK_PROCESSING_FAILED",
4177
- "Webhook processing failed; Stripe should retry. Processing is idempotent by event id."
5008
+ "Webhook processing failed; LlamaPay should retry. Processing is idempotent by event id."
4178
5009
  )
4179
5010
  );
4180
5011
  }
@@ -4183,20 +5014,39 @@ var stripeWebhookHandler = asyncHandler(
4183
5014
 
4184
5015
  // src/middleware/request-id.ts
4185
5016
  import { randomUUID as randomUUID2 } from "crypto";
5017
+ function isUnstampedApiError(body) {
5018
+ if (typeof body !== "object" || body === null) return false;
5019
+ const candidate = body;
5020
+ if (candidate.ok !== false) return false;
5021
+ if (typeof candidate.error !== "object" || candidate.error === null) {
5022
+ return false;
5023
+ }
5024
+ return !("request_id" in candidate.error);
5025
+ }
4186
5026
  function requestIdMiddleware(req, res, next) {
4187
5027
  const id = req.headers["x-request-id"] || `req_${randomUUID2().replace(/-/g, "")}`;
4188
5028
  req.requestId = id;
4189
5029
  res.setHeader("x-request-id", id);
5030
+ const json = res.json.bind(res);
5031
+ res.json = (body) => {
5032
+ if (isUnstampedApiError(body)) {
5033
+ body.error.request_id = id;
5034
+ }
5035
+ return json(body);
5036
+ };
4190
5037
  next();
4191
5038
  }
4192
5039
 
4193
5040
  // src/middleware/rate-limit-headers.ts
4194
5041
  function rateLimitInfoHeaders(opts) {
4195
5042
  const windowSec = Math.round(opts.windowMs / 1e3);
4196
- return (_req, res, next) => {
5043
+ return (req, res, next) => {
4197
5044
  if (!res.getHeader("RateLimit-Limit")) {
4198
- res.setHeader("RateLimit-Limit", String(opts.limit));
4199
- res.setHeader("RateLimit-Policy", `${opts.limit};w=${windowSec}`);
5045
+ const headers = req.headers ?? {};
5046
+ const hasCredential2 = typeof headers["x-api-key"] === "string" || typeof headers.authorization === "string" && headers.authorization.startsWith("Bearer ");
5047
+ const limit = hasCredential2 && opts.credentialedLimit !== void 0 ? opts.credentialedLimit : opts.limit;
5048
+ res.setHeader("RateLimit-Limit", String(limit));
5049
+ res.setHeader("RateLimit-Policy", `${limit};w=${windowSec}`);
4200
5050
  }
4201
5051
  next();
4202
5052
  };
@@ -4600,12 +5450,13 @@ import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
4600
5450
 
4601
5451
  // src/services/anon-quota.ts
4602
5452
  import { createHash as createHash4 } from "crypto";
4603
- import { z as z4 } from "zod";
5453
+ import { z as z3 } from "zod";
4604
5454
  var ANON_QUOTA_EXHAUSTED_MESSAGE = "Anonymous free quota reached (resets 00:00 UTC). For continued access set HIVE_API_KEY \u2014 create one free at https://hiveintelligence.xyz/dashboard/keys \u2014 or sign in via OAuth.";
5455
+ var ANON_GLOBAL_CAP_MESSAGE = "Hive's shared anonymous allowance for today is spent (resets 00:00 UTC). This is a service-wide cap, not your personal one, and a key is not rate-limited by it: set HIVE_API_KEY \u2014 create one free at https://hiveintelligence.xyz/dashboard/keys \u2014 or sign in via OAuth.";
4605
5456
  var ANON_QUOTA_UNAVAILABLE_MESSAGE = "Anonymous access is temporarily unavailable because the quota backend is unreachable. Retry in a moment, or set HIVE_API_KEY \u2014 create one free at https://hiveintelligence.xyz/dashboard/keys \u2014 for keyed access now.";
4606
5457
  var ANON_IP_HASH_DOMAIN = "hive-anon-lane-v1";
4607
- var capSchema = z4.coerce.number().int().min(1).max(1e4);
4608
- var globalCapSchema = z4.coerce.number().int().min(1).max(1e6);
5458
+ var capSchema = z3.coerce.number().int().min(1).max(1e4);
5459
+ var globalCapSchema = z3.coerce.number().int().min(1).max(1e6);
4609
5460
  function isAnonLaneEnabled() {
4610
5461
  return getEnvVar("ANON_LANE_ENABLED")?.toLowerCase() !== "false";
4611
5462
  }
@@ -4620,6 +5471,27 @@ function getAnonGlobalDailyCap() {
4620
5471
  function anonymousIpHash(ipKey) {
4621
5472
  return createHash4("sha256").update(`${ANON_IP_HASH_DOMAIN}\0${ipKey}`).digest("hex");
4622
5473
  }
5474
+ var GLOBAL_CAP_LOG_INTERVAL_MS = 6e4;
5475
+ var lastGlobalCapLoggedAt = 0;
5476
+ var globalCapRejections = 0;
5477
+ var globalCapRejectionsAtLastLog = 0;
5478
+ function noteGlobalCapTripped(limit, resetSeconds) {
5479
+ globalCapRejections += 1;
5480
+ const now = Date.now();
5481
+ if (now - lastGlobalCapLoggedAt < GLOBAL_CAP_LOG_INTERVAL_MS) return;
5482
+ lastGlobalCapLoggedAt = now;
5483
+ const rejected = globalCapRejections - globalCapRejectionsAtLastLog;
5484
+ globalCapRejectionsAtLastLog = globalCapRejections;
5485
+ secureLogger.error(
5486
+ "[anon-lane] GLOBAL daily breaker tripped \u2014 every anonymous caller is blocked until the UTC day rolls",
5487
+ void 0,
5488
+ {
5489
+ globalDailyCap: limit,
5490
+ resetSeconds,
5491
+ rejectionsSinceLastLog: rejected
5492
+ }
5493
+ );
5494
+ }
4623
5495
  async function defaultConsume(args) {
4624
5496
  const { data, error } = await getSupabaseAdmin().rpc(
4625
5497
  "consume_anon_daily_quota",
@@ -4660,11 +5532,12 @@ async function consumeAnonMaterialQuota(ipHash, overrides = {}) {
4660
5532
  resetSeconds: Math.max(1, row.reset_seconds ?? 1)
4661
5533
  };
4662
5534
  }
4663
- return {
4664
- allowed: false,
4665
- reason: row.scope === "global" ? "global_cap" : "ip_cap",
4666
- resetSeconds: Math.max(1, row.reset_seconds ?? 1)
4667
- };
5535
+ const resetSeconds = Math.max(1, row.reset_seconds ?? 1);
5536
+ if (row.scope === "global") {
5537
+ noteGlobalCapTripped(getAnonGlobalDailyCap(), resetSeconds);
5538
+ return { allowed: false, reason: "global_cap", resetSeconds };
5539
+ }
5540
+ return { allowed: false, reason: "ip_cap", resetSeconds };
4668
5541
  } catch (err) {
4669
5542
  secureLogger.warn("Anonymous quota backend unavailable; failing closed", {
4670
5543
  error: err instanceof Error ? err.message : String(err)
@@ -4693,7 +5566,7 @@ function buildServerCard() {
4693
5566
  email: "support@hiveintelligence.xyz"
4694
5567
  },
4695
5568
  homepage: "https://www.hiveintelligence.xyz",
4696
- documentation: "https://www.hiveintelligence.xyz/crypto-mcp",
5569
+ documentation: "https://docs.hiveintelligence.xyz/concepts/how-it-works",
4697
5570
  repository: {
4698
5571
  type: "git",
4699
5572
  url: "https://github.com/hive-intel/hive-sdk"
@@ -4926,33 +5799,33 @@ function buildServerCard() {
4926
5799
  }
4927
5800
 
4928
5801
  // src/auth/oauth/router.ts
4929
- import { timingSafeEqual as timingSafeEqual4 } from "crypto";
5802
+ import { timingSafeEqual as timingSafeEqual5 } from "crypto";
4930
5803
  import express from "express";
4931
- import rateLimit3 from "express-rate-limit";
4932
- import RedisStore3 from "rate-limit-redis";
4933
- import { z as z5 } from "zod";
5804
+ import rateLimit2 from "express-rate-limit";
5805
+ import RedisStore2 from "rate-limit-redis";
5806
+ import { z as z4 } from "zod";
4934
5807
  import {
4935
5808
  createOAuthMetadata,
4936
5809
  getOAuthProtectedResourceMetadataUrl,
4937
5810
  mcpAuthRouter
4938
5811
  } from "@modelcontextprotocol/sdk/server/auth/router.js";
4939
5812
  import { OAuthError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
4940
- var consentDecisionSchema = z5.object({
4941
- approved: z5.boolean(),
4942
- scopes: z5.array(z5.enum(OAUTH_SCOPES)).max(OAUTH_SCOPES.length).optional()
5813
+ var consentDecisionSchema = z4.object({
5814
+ approved: z4.boolean(),
5815
+ scopes: z4.array(z4.enum(OAUTH_SCOPES)).max(OAUTH_SCOPES.length).optional()
4943
5816
  }).strict();
4944
- var consentRequestIdSchema = z5.string().min(40).max(100).regex(/^hvoq_[A-Za-z0-9_-]+$/);
4945
- var connectedAppDisconnectSchema = z5.object({
4946
- client_id: z5.string().min(1).max(2048).refine((value) => value.trim() === value, {
5817
+ var consentRequestIdSchema = z4.string().min(40).max(100).regex(/^hvoq_[A-Za-z0-9_-]+$/);
5818
+ var connectedAppDisconnectSchema = z4.object({
5819
+ client_id: z4.string().min(1).max(2048).refine((value) => value.trim() === value, {
4947
5820
  message: "client_id must not contain leading or trailing whitespace."
4948
5821
  })
4949
5822
  }).strict();
4950
- var deploymentCanaryClientIdSchema = z5.string().regex(/^hive_oauth_client_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
4951
- var deploymentCanaryConsentSchema = z5.object({
5823
+ var deploymentCanaryClientIdSchema = z4.string().regex(/^hive_oauth_client_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
5824
+ var deploymentCanaryConsentSchema = z4.object({
4952
5825
  request_id: consentRequestIdSchema,
4953
5826
  client_id: deploymentCanaryClientIdSchema
4954
5827
  }).strict();
4955
- var deploymentCanaryDisconnectSchema = z5.object({ client_id: deploymentCanaryClientIdSchema }).strict();
5828
+ var deploymentCanaryDisconnectSchema = z4.object({ client_id: deploymentCanaryClientIdSchema }).strict();
4956
5829
  var OAUTH_RATE_LIMITS = {
4957
5830
  authorize: { windowMs: 15 * 60 * 1e3, max: 300 },
4958
5831
  token: { windowMs: 15 * 60 * 1e3, max: 3e3 },
@@ -4971,7 +5844,7 @@ function oauthRateLimitOptions(prefix) {
4971
5844
  const redis = getRedisClient();
4972
5845
  return {
4973
5846
  ...redis ? {
4974
- store: new RedisStore3({
5847
+ store: new RedisStore2({
4975
5848
  sendCommand: ((...args) => redis.call(
4976
5849
  args[0],
4977
5850
  ...args.slice(1)
@@ -4988,11 +5861,11 @@ function monitorSecretAuthorized(req) {
4988
5861
  if (!expected || typeof supplied !== "string") return false;
4989
5862
  const left = Buffer.from(supplied);
4990
5863
  const right = Buffer.from(expected);
4991
- return left.length === right.length && timingSafeEqual4(left, right);
5864
+ return left.length === right.length && timingSafeEqual5(left, right);
4992
5865
  }
4993
5866
  function deploymentCanaryUserId() {
4994
5867
  const userId = getEnvVar("HIVE_CANARY_USER_ID");
4995
- return userId && z5.string().uuid().safeParse(userId).success ? userId : void 0;
5868
+ return userId && z4.string().uuid().safeParse(userId).success ? userId : void 0;
4996
5869
  }
4997
5870
  async function verifyDeploymentCanaryUser(userId) {
4998
5871
  const { data, error } = await getSupabaseAdmin().auth.admin.getUserById(userId);
@@ -5010,7 +5883,7 @@ function createMcpOAuthRuntime(options = {}) {
5010
5883
  { fetcher: options.fetcher }
5011
5884
  );
5012
5885
  const serviceDocumentationUrl = new URL(
5013
- "https://www.hiveintelligence.xyz/crypto-mcp"
5886
+ "https://docs.hiveintelligence.xyz/concepts/how-it-works"
5014
5887
  );
5015
5888
  const oauthMetadata = {
5016
5889
  ...createOAuthMetadata({
@@ -5081,7 +5954,7 @@ function createMcpOAuthRuntime(options = {}) {
5081
5954
  res.json(resourceMetadata);
5082
5955
  }
5083
5956
  );
5084
- const consentRateLimit = rateLimit3({
5957
+ const consentRateLimit = rateLimit2({
5085
5958
  ...oauthRateLimitOptions("rl:oauth:consent:"),
5086
5959
  ...OAUTH_RATE_LIMITS.account,
5087
5960
  standardHeaders: true,
@@ -5107,7 +5980,7 @@ function createMcpOAuthRuntime(options = {}) {
5107
5980
  ).href;
5108
5981
  const deploymentCanaryJsonParser = express.json({ limit: "8kb" });
5109
5982
  const deploymentCanaryUserVerifier = options.deploymentCanaryUserVerifier ?? verifyDeploymentCanaryUser;
5110
- const deploymentCanaryRateLimit = rateLimit3({
5983
+ const deploymentCanaryRateLimit = rateLimit2({
5111
5984
  ...oauthRateLimitOptions("rl:oauth:deployment-canary:"),
5112
5985
  ...OAUTH_RATE_LIMITS.deploymentCanary,
5113
5986
  standardHeaders: true,
@@ -5739,10 +6612,12 @@ app.use((_req, res, next) => {
5739
6612
  return next();
5740
6613
  });
5741
6614
  var RATE_LIMIT_MAX = getRateLimitMax();
6615
+ var CREDENTIALED_RATE_LIMIT_MAX = getCredentialedRateLimitMax();
5742
6616
  var RATE_LIMIT_WINDOW_MS = 6e4;
5743
6617
  app.use(
5744
6618
  rateLimitInfoHeaders({
5745
6619
  limit: RATE_LIMIT_MAX,
6620
+ credentialedLimit: CREDENTIALED_RATE_LIMIT_MAX,
5746
6621
  windowMs: RATE_LIMIT_WINDOW_MS
5747
6622
  })
5748
6623
  );
@@ -5829,6 +6704,11 @@ app.post(
5829
6704
  express2.raw({ type: "application/json", limit: "1mb" }),
5830
6705
  stripeWebhookHandler
5831
6706
  );
6707
+ app.post(
6708
+ "/api/v1/billing/webhook/llamapay",
6709
+ express2.raw({ type: "application/json", limit: "1mb" }),
6710
+ llamapayWebhookHandler
6711
+ );
5832
6712
  app.use(
5833
6713
  express2.json({
5834
6714
  limit: "1mb",
@@ -5982,10 +6862,20 @@ function pingHandler(_req, res) {
5982
6862
  }
5983
6863
  app.get("/ping", pingHandler);
5984
6864
  app.get("/health", pingHandler);
5985
- var mcpIpRateLimiter = createIpRateLimiter();
5986
- var restIpRateLimiter = createIpRateLimiter();
5987
- var authIpRateLimiter = createIpRateLimiter();
5988
- app.use(["/status", "/api/health"], restIpRateLimiter);
6865
+ var mcpIpRateLimiter = [
6866
+ createIpRateLimiter({ lane: "anonymous" }),
6867
+ createIpRateLimiter({ lane: "credentialed" })
6868
+ ];
6869
+ var restIpRateLimiter = [
6870
+ createIpRateLimiter({ lane: "anonymous" }),
6871
+ createIpRateLimiter({ lane: "credentialed" })
6872
+ ];
6873
+ var authIpRateLimiter = [
6874
+ createIpRateLimiter({ lane: "anonymous" }),
6875
+ createIpRateLimiter({ lane: "credentialed" })
6876
+ ];
6877
+ var healthRateLimiter = createLocalRateLimiter();
6878
+ app.use(["/status", "/api/health"], healthRateLimiter);
5989
6879
  app.use(health_default);
5990
6880
  app.use("/auth", authIpRateLimiter);
5991
6881
  app.use("/api/v1", restIpRateLimiter);
@@ -5994,7 +6884,7 @@ app.use("/api/v1/keys", keys_default);
5994
6884
  app.use("/api/v1/usage", usage_default);
5995
6885
  app.use("/api/v1/b2b", b2b_default);
5996
6886
  app.use("/api/v1/billing", billingRouter);
5997
- var mcpMiddlewares = [mcpIpRateLimiter];
6887
+ var mcpMiddlewares = [...mcpIpRateLimiter];
5998
6888
  function withTimeout(p, ms) {
5999
6889
  return new Promise((resolve2, reject) => {
6000
6890
  const timer = setTimeout(() => {
@@ -6012,6 +6902,16 @@ function withTimeout(p, ms) {
6012
6902
  );
6013
6903
  });
6014
6904
  }
6905
+ var PRICING_URL = "https://www.hiveintelligence.xyz/pricing";
6906
+ function buildMcpQuotaExhaustedCopy(args) {
6907
+ const plan = args.planId ? `${args.planId} plan` : "current plan";
6908
+ const reset = args.cycleEnd ? `Credits refill at ${args.cycleEnd}.` : "Credits refill at the start of the next billing cycle.";
6909
+ return {
6910
+ message: `Monthly credit quota exhausted on the ${plan}. ${reset} Upgrade at ${PRICING_URL} to raise the limit now, or wait for the cycle to reset.`,
6911
+ cause: `The ${plan} has no monthly credits left, so this material call cannot be debited.`,
6912
+ next_action: `Upgrade at ${PRICING_URL} to raise the monthly allowance, or retry after the cycle resets${args.cycleEnd ? ` at ${args.cycleEnd}` : ""}. Discovery calls (tools/list, search_tools, schema lookup) cost no credits and still work.`
6913
+ };
6914
+ }
6015
6915
  async function debitMcpCredits(args) {
6016
6916
  try {
6017
6917
  const debit = await consumeCredits({
@@ -6029,9 +6929,15 @@ async function debitMcpCredits(args) {
6029
6929
  };
6030
6930
  }
6031
6931
  if (debit.code === "insufficient") {
6932
+ const copy = buildMcpQuotaExhaustedCopy({
6933
+ planId: args.planId,
6934
+ cycleEnd: debit.cycleEnd
6935
+ });
6032
6936
  return {
6033
6937
  ok: false,
6034
- error: "Credit quota exceeded. Add credits or upgrade your plan to continue.",
6938
+ error: copy.message,
6939
+ cause: copy.cause,
6940
+ next_action: copy.next_action,
6035
6941
  rateLimited: true
6036
6942
  };
6037
6943
  }
@@ -6250,6 +7156,16 @@ function getMcpHiveToolName(body) {
6250
7156
  }
6251
7157
  return toolName.startsWith("hive_") ? toolName : void 0;
6252
7158
  }
7159
+ function resolveMcpClientName(req, body) {
7160
+ const fromBody = body?.params?.clientInfo?.name;
7161
+ if (typeof fromBody === "string" && fromBody.trim()) return fromBody.trim();
7162
+ const header = req.headers["x-hive-client"];
7163
+ if (typeof header === "string" && header.trim()) return header.trim();
7164
+ const userAgent = req.headers["user-agent"];
7165
+ if (typeof userAgent !== "string") return void 0;
7166
+ const product = userAgent.trim().split(/[\s\/]/, 1)[0];
7167
+ return product || void 0;
7168
+ }
6253
7169
  function mcpCredentialPresented(req) {
6254
7170
  return typeof req.headers["x-api-key"] === "string" || Boolean(req.headers.authorization?.startsWith("Bearer "));
6255
7171
  }
@@ -6288,7 +7204,11 @@ async function authorizeAnonymousMcpRequest(req) {
6288
7204
  };
6289
7205
  }
6290
7206
  if (quota.reason === "unavailable") return { kind: "quota_unavailable" };
6291
- return { kind: "quota_exhausted", resetSeconds: quota.resetSeconds };
7207
+ return {
7208
+ kind: "quota_exhausted",
7209
+ scope: quota.reason === "global_cap" ? "global" : "ip",
7210
+ resetSeconds: quota.resetSeconds
7211
+ };
6292
7212
  }
6293
7213
  function isMonitorSecretRequest(req) {
6294
7214
  const secret = getEnvVar("MONITOR_SECRET");
@@ -6296,7 +7216,7 @@ function isMonitorSecretRequest(req) {
6296
7216
  if (!secret || typeof provided !== "string") return false;
6297
7217
  const a = Buffer.from(provided);
6298
7218
  const b = Buffer.from(secret);
6299
- return a.length === b.length && timingSafeEqual5(a, b);
7219
+ return a.length === b.length && timingSafeEqual6(a, b);
6300
7220
  }
6301
7221
  function getValidatedMcpHiveToolName(scope, body) {
6302
7222
  if (body?.method !== "tools/call") return void 0;
@@ -6482,6 +7402,7 @@ function createStatelessMcpHandler(scope) {
6482
7402
  }
6483
7403
  } else if (decision.kind === "quota_exhausted") {
6484
7404
  res.setHeader("Retry-After", String(decision.resetSeconds));
7405
+ const exhaustedMessage = decision.scope === "global" ? ANON_GLOBAL_CAP_MESSAGE : ANON_QUOTA_EXHAUSTED_MESSAGE;
6485
7406
  if (mcpOAuth.status === "ready") {
6486
7407
  const requiredScopes = getRequiredMcpScopes(req.body);
6487
7408
  res.setHeader(
@@ -6493,7 +7414,7 @@ function createStatelessMcpHandler(scope) {
6493
7414
  );
6494
7415
  res.status(401).json({
6495
7416
  error: "authorization_required",
6496
- error_description: ANON_QUOTA_EXHAUSTED_MESSAGE
7417
+ error_description: exhaustedMessage
6497
7418
  });
6498
7419
  return;
6499
7420
  }
@@ -6501,9 +7422,9 @@ function createStatelessMcpHandler(scope) {
6501
7422
  jsonrpc: "2.0",
6502
7423
  error: {
6503
7424
  code: -32001,
6504
- message: ANON_QUOTA_EXHAUSTED_MESSAGE,
7425
+ message: exhaustedMessage,
6505
7426
  data: {
6506
- code: "ANON_QUOTA_EXCEEDED",
7427
+ code: decision.scope === "global" ? "ANON_GLOBAL_CAP_EXCEEDED" : "ANON_QUOTA_EXCEEDED",
6507
7428
  retryable: true,
6508
7429
  retry_after: decision.resetSeconds
6509
7430
  }
@@ -6601,6 +7522,30 @@ function createStatelessMcpHandler(scope) {
6601
7522
  return;
6602
7523
  }
6603
7524
  }
7525
+ if (keyedAuth?.userId) {
7526
+ const decision = await enforceUserRateLimit(keyedAuth.userId);
7527
+ res.setHeader("RateLimit-Limit", String(decision.limit));
7528
+ res.setHeader("RateLimit-Remaining", String(decision.remaining));
7529
+ res.setHeader("RateLimit-Reset", String(decision.resetSeconds));
7530
+ res.setHeader("RateLimit-Policy", `${decision.limit};w=60`);
7531
+ if (!decision.allowed) {
7532
+ res.setHeader("Retry-After", String(decision.resetSeconds));
7533
+ res.status(200).json({
7534
+ jsonrpc: "2.0",
7535
+ error: {
7536
+ code: -32001,
7537
+ message: `Rate limit exceeded: ${decision.limit} requests per minute on your plan. This limit is shared across all of your API keys and connected clients.`,
7538
+ data: {
7539
+ code: "RATE_LIMITED",
7540
+ retryable: true,
7541
+ retry_after: decision.resetSeconds
7542
+ }
7543
+ },
7544
+ id: req.body?.id ?? null
7545
+ });
7546
+ return;
7547
+ }
7548
+ }
6604
7549
  try {
6605
7550
  const body = req.body;
6606
7551
  const rawHiveToolName = getMcpHiveToolName(body);
@@ -6651,11 +7596,10 @@ function createStatelessMcpHandler(scope) {
6651
7596
  subjectContext = subject.data;
6652
7597
  }
6653
7598
  sanitizeMcpAuthContext(body);
6654
- const clientInfoName = body?.params?.clientInfo?.name;
6655
7599
  const analytics = {
6656
7600
  principalRaw: keyedAuth?.userId ?? anonymousLane?.principalRaw,
6657
7601
  authLane: keyedAuth ? keyedAuth.authMethod : "anonymous",
6658
- clientName: typeof clientInfoName === "string" ? clientInfoName : void 0,
7602
+ clientName: resolveMcpClientName(req, body),
6659
7603
  isInternal: isMonitorSecretRequest(req)
6660
7604
  };
6661
7605
  const server = createHiveMcpServer(scope, {
@@ -6719,7 +7663,8 @@ function createStatelessMcpHandler(scope) {
6719
7663
  const debit = await debitMcpCredits({
6720
7664
  userId: keyedAuth.userId,
6721
7665
  keyId: keyedAuth.keyId,
6722
- requestId: req.requestId
7666
+ requestId: req.requestId,
7667
+ planId: typeof keyedAuth.keyMeta?.plan === "string" ? keyedAuth.keyMeta.plan : void 0
6723
7668
  });
6724
7669
  if (!debit.ok) {
6725
7670
  if (debit.noWallet) {
@@ -6737,7 +7682,9 @@ function createStatelessMcpHandler(scope) {
6737
7682
  type: debit.rateLimited ? "rate_limit_error" : "internal_error",
6738
7683
  code: debit.rateLimited ? "QUOTA_EXCEEDED" : "QUOTA_SERVICE_UNAVAILABLE",
6739
7684
  message: debit.error,
6740
- runtimeStatus: debit.rateLimited ? "rate_limited" : "failing"
7685
+ runtimeStatus: debit.rateLimited ? "rate_limited" : "failing",
7686
+ ...debit.cause ? { cause: debit.cause } : {},
7687
+ ...debit.next_action ? { next_action: debit.next_action } : {}
6741
7688
  };
6742
7689
  }
6743
7690
  res.setHeader(
@@ -6756,16 +7703,18 @@ function createStatelessMcpHandler(scope) {
6756
7703
  };
6757
7704
  }
6758
7705
  }
6759
- if (subjectContext && isHiveStatefulWriteTool(executedToolName)) {
6760
- recordSubjectAuditEvent({
6761
- userId: subjectContext.userId,
6762
- subjectId: subjectContext.subjectId,
6763
- keyId: keyedAuth.keyId,
6764
- transport: "mcp",
6765
- toolName: executedToolName,
6766
- requestId: req.requestId
6767
- });
6768
- }
7706
+ const auditEvent = buildStatefulWriteAuditEvent({
7707
+ isStatefulWrite: isHiveStatefulWriteTool(executedToolName),
7708
+ transport: "mcp",
7709
+ toolName: executedToolName,
7710
+ userId: keyedAuth.userId,
7711
+ subjectContext,
7712
+ keyId: keyedAuth.keyId,
7713
+ authMethod: keyedAuth.authMethod,
7714
+ oauthClientId: keyedAuth.oauthClientId,
7715
+ requestId: req.requestId
7716
+ });
7717
+ if (auditEvent) recordSubjectAuditEvent(auditEvent);
6769
7718
  return void 0;
6770
7719
  } : anonymousLane ? async (executedToolName) => {
6771
7720
  const creditCost = getMcpCreditCost(executedToolName);
@@ -6868,7 +7817,7 @@ var requireMcpTransportCredential = async (req, res, next) => {
6868
7817
  };
6869
7818
  app.get(
6870
7819
  mcpPaths,
6871
- mcpIpRateLimiter,
7820
+ ...mcpIpRateLimiter,
6872
7821
  requireMcpTransportCredential,
6873
7822
  (_req, res) => {
6874
7823
  res.writeHead(405, { Allow: "POST" }).end();
@@ -6876,7 +7825,7 @@ app.get(
6876
7825
  );
6877
7826
  app.delete(
6878
7827
  mcpPaths,
6879
- mcpIpRateLimiter,
7828
+ ...mcpIpRateLimiter,
6880
7829
  requireMcpTransportCredential,
6881
7830
  (_req, res) => {
6882
7831
  res.writeHead(405, { Allow: "POST" }).end();
@@ -6884,7 +7833,7 @@ app.delete(
6884
7833
  );
6885
7834
  app.post(
6886
7835
  "/api/run-tests",
6887
- restIpRateLimiter,
7836
+ ...restIpRateLimiter,
6888
7837
  asyncHandler(async (req, res) => {
6889
7838
  const authToken = req.headers["x-monitor-secret"];
6890
7839
  const monitorSecret = getEnvVar("MONITOR_SECRET");
@@ -6892,7 +7841,7 @@ app.post(
6892
7841
  if (monitorSecret && typeof authToken === "string") {
6893
7842
  const a = Buffer.from(authToken);
6894
7843
  const b = Buffer.from(monitorSecret);
6895
- isAuthorized = a.length === b.length && timingSafeEqual5(a, b);
7844
+ isAuthorized = a.length === b.length && timingSafeEqual6(a, b);
6896
7845
  }
6897
7846
  secureLogger.authEvent("monitor-tests-access", isAuthorized, {
6898
7847
  hasToken: !!authToken,
@@ -7041,15 +7990,50 @@ function getRestHeroEndpoint(toolName) {
7041
7990
  }
7042
7991
  return restHeroEndpointsByName.get(toolName);
7043
7992
  }
7993
+ var knownRestToolNames = null;
7994
+ function isKnownRestToolName(toolName) {
7995
+ if (!knownRestToolNames) {
7996
+ knownRestToolNames = new Set(
7997
+ getAllWrappedEndpoints().map((endpoint) => endpoint.tool.name)
7998
+ );
7999
+ }
8000
+ return knownRestToolNames.has(toolName);
8001
+ }
7044
8002
  var anonymousHeroExecuteMiddleware = asyncHandler(
7045
8003
  async (req, res, next) => {
8004
+ const rawBody = req.body;
8005
+ if (rawBody && typeof rawBody === "object" && rawBody.args === void 0 && rawBody.arguments !== void 0) {
8006
+ rawBody.args = rawBody.arguments;
8007
+ }
7046
8008
  if (mcpCredentialPresented(req)) {
7047
8009
  next();
7048
8010
  return;
7049
8011
  }
7050
- const body = req.body;
8012
+ const body = rawBody;
7051
8013
  const toolName = typeof body?.tool === "string" ? body.tool : void 0;
7052
- if (!toolName || !HERO_TOOL_NAMES.has(toolName)) {
8014
+ if (!toolName) {
8015
+ res.status(400).json({
8016
+ ok: false,
8017
+ error: {
8018
+ type: "invalid_request",
8019
+ code: "TOOL_REQUIRED",
8020
+ message: `Missing required field "tool". Send {"tool":"<name>","args":{...}}. The keyless lane serves ${[...HERO_TOOL_NAMES].join(", ")}; every other tool needs an API key.`
8021
+ }
8022
+ });
8023
+ return;
8024
+ }
8025
+ if (!HERO_TOOL_NAMES.has(toolName) && !isKnownRestToolName(toolName)) {
8026
+ res.status(404).json({
8027
+ ok: false,
8028
+ error: {
8029
+ type: "invalid_request",
8030
+ code: "TOOL_NOT_FOUND",
8031
+ message: `Unknown tool "${toolName}". Browse the catalog at /api/v1/tools, or call search_tools over MCP.`
8032
+ }
8033
+ });
8034
+ return;
8035
+ }
8036
+ if (!HERO_TOOL_NAMES.has(toolName)) {
7053
8037
  next();
7054
8038
  return;
7055
8039
  }
@@ -7077,12 +8061,13 @@ var anonymousHeroExecuteMiddleware = asyncHandler(
7077
8061
  );
7078
8062
  return;
7079
8063
  }
8064
+ const globalCap = quota.reason === "global_cap";
7080
8065
  res.setHeader("Retry-After", String(quota.resetSeconds));
7081
8066
  res.status(429).json(
7082
8067
  apiError(
7083
8068
  "rate_limit_error",
7084
- "ANON_QUOTA_EXCEEDED",
7085
- ANON_QUOTA_EXHAUSTED_MESSAGE,
8069
+ globalCap ? "ANON_GLOBAL_CAP_EXCEEDED" : "ANON_QUOTA_EXCEEDED",
8070
+ globalCap ? ANON_GLOBAL_CAP_MESSAGE : ANON_QUOTA_EXHAUSTED_MESSAGE,
7086
8071
  { requestId: req.requestId }
7087
8072
  )
7088
8073
  );
@@ -7096,6 +8081,7 @@ var anonymousHeroExecuteMiddleware = asyncHandler(
7096
8081
  analytics: {
7097
8082
  principalRaw: `anon:${ipHash}`,
7098
8083
  authLane: "anonymous",
8084
+ clientName: resolveMcpClientName(req, body),
7099
8085
  isInternal: isMonitorSecretRequest(req)
7100
8086
  }
7101
8087
  });
@@ -7168,16 +8154,18 @@ app.post(
7168
8154
  };
7169
8155
  }
7170
8156
  }
7171
- if (authReq.hiveSubjectContext && isHiveStatefulWriteTool(endpoint.tool.name)) {
7172
- recordSubjectAuditEvent({
7173
- userId: authReq.hiveSubjectContext.userId,
7174
- subjectId: authReq.hiveSubjectContext.subjectId,
7175
- keyId: authReq.unkeyKeyId,
7176
- transport: "rest",
7177
- toolName: endpoint.tool.name,
7178
- requestId: req.requestId
7179
- });
7180
- }
8157
+ const restAuditEvent = buildStatefulWriteAuditEvent({
8158
+ isStatefulWrite: isHiveStatefulWriteTool(endpoint.tool.name),
8159
+ transport: "rest",
8160
+ toolName: endpoint.tool.name,
8161
+ userId: authReq.userId,
8162
+ subjectContext: authReq.hiveSubjectContext,
8163
+ keyId: authReq.unkeyKeyId,
8164
+ // REST is the API-key transport; OAuth tokens are MCP-only.
8165
+ authMethod: "api_key",
8166
+ requestId: req.requestId
8167
+ });
8168
+ if (restAuditEvent) recordSubjectAuditEvent(restAuditEvent);
7181
8169
  return void 0;
7182
8170
  };
7183
8171
  const heroEndpoint = HERO_TOOL_NAMES.has(executionToolName) ? getRestHeroEndpoint(executionToolName) : void 0;
@@ -7229,13 +8217,13 @@ app.post(
7229
8217
  });
7230
8218
  })
7231
8219
  );
7232
- var cliTelemetrySchema = z6.object({
7233
- command: z6.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9:_-]+$/),
7234
- durationMs: z6.number().int().min(0).max(6e5),
7235
- errorCode: z6.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9:_-]+$/).optional(),
7236
- cliVersion: z6.string().trim().min(1).max(40),
7237
- os: z6.string().trim().min(1).max(120),
7238
- nodeVersion: z6.string().trim().min(1).max(40)
8220
+ var cliTelemetrySchema = z5.object({
8221
+ command: z5.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9:_-]+$/),
8222
+ durationMs: z5.number().int().min(0).max(6e5),
8223
+ errorCode: z5.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9:_-]+$/).optional(),
8224
+ cliVersion: z5.string().trim().min(1).max(40),
8225
+ os: z5.string().trim().min(1).max(120),
8226
+ nodeVersion: z5.string().trim().min(1).max(40)
7239
8227
  }).strict();
7240
8228
  app.post("/api/v1/telemetry", (req, res) => {
7241
8229
  const parsed = cliTelemetrySchema.safeParse(req.body);
@@ -7324,6 +8312,10 @@ async function setupServer() {
7324
8312
  `OAuth configuration is incomplete: ${mcpOAuth.config.issues.join(" ")}`
7325
8313
  );
7326
8314
  }
8315
+ const redisReady = await waitForRedisAvailability(3e3);
8316
+ secureLogger.info("[startup] Redis readiness settled", {
8317
+ ready: redisReady
8318
+ });
7327
8319
  secureLogger.info("Setting up Hive MCP server...", {
7328
8320
  server: SERVER_NAME,
7329
8321
  version: SERVER_VERSION,
@@ -7420,21 +8412,41 @@ async function startHttpServer() {
7420
8412
  });
7421
8413
  httpServer.keepAliveTimeout = 65e3;
7422
8414
  httpServer.headersTimeout = 7e4;
8415
+ const ANALYTICS_FLUSH_TIMEOUT_MS = 2e3;
8416
+ const flushAnalytics = async () => {
8417
+ const writer = getAnalyticsWriter();
8418
+ await Promise.race([
8419
+ writer.flush(),
8420
+ new Promise((resolve2) => {
8421
+ setTimeout(resolve2, ANALYTICS_FLUSH_TIMEOUT_MS).unref();
8422
+ })
8423
+ ]);
8424
+ const stats = writer.getStats();
8425
+ secureLogger.info("Analytics queue flushed on shutdown", {
8426
+ stillQueued: stats.queued,
8427
+ droppedTotal: stats.dropped
8428
+ });
8429
+ };
8430
+ const exitAfterAnalyticsFlush = (code) => {
8431
+ getAnalyticsWriter().stop();
8432
+ void flushAnalytics().finally(() => process.exit(code));
8433
+ };
7423
8434
  const shutdown = (signal) => {
7424
8435
  if (isShuttingDown) return;
7425
8436
  isShuttingDown = true;
7426
8437
  secureLogger.info(
7427
8438
  `${signal} received \u2014 draining ${activeRequests} active requests`
7428
8439
  );
8440
+ void flushAnalytics();
7429
8441
  httpServer.close(() => {
7430
8442
  secureLogger.info("HTTP server closed", { activeRequests });
7431
- process.exit(0);
8443
+ exitAfterAnalyticsFlush(0);
7432
8444
  });
7433
8445
  const drainInterval = setInterval(() => {
7434
8446
  if (activeRequests <= 0) {
7435
8447
  clearInterval(drainInterval);
7436
8448
  secureLogger.info("All requests drained, closing");
7437
- httpServer.close(() => process.exit(0));
8449
+ httpServer.close(() => exitAfterAnalyticsFlush(0));
7438
8450
  }
7439
8451
  }, 500);
7440
8452
  drainInterval.unref();
@@ -7443,7 +8455,7 @@ async function startHttpServer() {
7443
8455
  secureLogger.warn("Force shutdown after 10s timeout", {
7444
8456
  activeRequests
7445
8457
  });
7446
- process.exit(1);
8458
+ exitAfterAnalyticsFlush(1);
7447
8459
  }, 1e4);
7448
8460
  forceTimer.unref();
7449
8461
  };
@@ -7466,9 +8478,11 @@ if (isMainModule()) {
7466
8478
  }
7467
8479
  var server_default = app;
7468
8480
  export {
8481
+ buildMcpQuotaExhaustedCopy,
7469
8482
  classifyAnonymousMcpRequest,
7470
8483
  createHttpApp,
7471
8484
  server_default as default,
7472
8485
  getRequiredMcpScopes,
8486
+ resolveMcpClientName,
7473
8487
  startHttpServer
7474
8488
  };