hive-intelligence 1.3.0 → 1.5.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 (49) hide show
  1. package/README.md +2 -2
  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-Y4GXW376.js} +1 -9
  22. package/build/browser-auth-F62BMD7W.js +20 -0
  23. package/build/{chunk-W2QL7LYI.js → chunk-AGM2CIRL.js} +971 -117
  24. package/build/{chunk-7GD2MFYI.js → chunk-BDO27NIP.js} +37 -17
  25. package/build/{upgrade-J4HIGCKE.js → chunk-GB4XMTDE.js} +3 -4
  26. package/build/{chunk-TNQ7GJ6Q.js → chunk-GWZIAAQ2.js} +58 -40
  27. package/build/{chunk-Q7VBYLX7.js → chunk-P7NLFSZQ.js} +0 -58
  28. package/build/{chunk-N32UIHBU.js → chunk-SFN772RL.js} +5 -5
  29. package/build/{chunk-AEU43ACS.js → chunk-TVBPCELA.js} +2 -2
  30. package/build/{chunk-RUHO24SX.js → chunk-WF34ZD5I.js} +7 -162
  31. package/build/{chunk-I5Z6FZRL.js → chunk-XQPEFCSY.js} +14 -12
  32. package/build/cli.js +36 -47
  33. package/build/{completion-7KBW243H.js → completion-C57PDJMY.js} +1 -1
  34. package/build/{doctor-ORDOCGLM.js → doctor-XG5IQCXK.js} +5 -5
  35. package/build/{init-UCTCUYFA.js → init-COHA3VZI.js} +3 -3
  36. package/build/{init-all-BVWW3JVK.js → init-all-YTQJLPLR.js} +43 -24
  37. package/build/{mcpServer-RUEOTTF7.js → mcpServer-XHIN5JER.js} +1 -1
  38. package/build/monitor-worker.js +2 -2
  39. package/build/{namespace-L5DGCY7K.js → namespace-YLCGHVRT.js} +1 -1
  40. package/build/release.json +4 -4
  41. package/build/{serve-DKLR2WQH.js → serve-PWMT5IRU.js} +1 -1
  42. package/build/server.js +1475 -452
  43. package/build/stdio.js +1 -1
  44. package/build/{tools-I7GLWTV6.js → tools-XNFJBN7Q.js} +2 -2
  45. package/build/{uninstall-SAOIPL6K.js → uninstall-TKDP6C4F.js} +7 -7
  46. package/build/upgrade-ODPKMIDC.js +14 -0
  47. package/build/{watch-WVR4CGCD.js → watch-MXJSNGL2.js} +2 -2
  48. package/package.json +5 -3
  49. 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-XQPEFCSY.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-AGM2CIRL.js";
97
99
  import {
98
100
  HIVE_AGENT_SKILLS
99
101
  } from "./chunk-L326MQZP.js";
@@ -733,7 +735,16 @@ 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
+
739
+ // src/config/trustProxy.ts
740
+ var DEFAULT_TRUSTED_PROXY_ADDRESSES = "34.102.187.55";
741
+ function resolveTrustedProxies(raw = getEnvVar("TRUSTED_PROXY_ADDRESSES")) {
742
+ const addresses = (raw ?? DEFAULT_TRUSTED_PROXY_ADDRESSES).split(",").map((entry) => entry.trim()).filter(Boolean);
743
+ return [...addresses, "loopback", "uniquelocal", "169.254.0.0/16"];
744
+ }
745
+
746
+ // src/server.ts
747
+ import { randomUUID as randomUUID4, timingSafeEqual as timingSafeEqual6 } from "crypto";
737
748
  import { resolve } from "path";
738
749
  import { fileURLToPath } from "url";
739
750
  import { createRequire as createNodeRequire } from "module";
@@ -741,7 +752,7 @@ import express2 from "express";
741
752
  import compression from "compression";
742
753
  import cors from "cors";
743
754
  import helmet from "helmet";
744
- import { z as z6 } from "zod";
755
+ import { z as z5 } from "zod";
745
756
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
746
757
 
747
758
  // src/middleware/rate-limit.ts
@@ -805,8 +816,43 @@ function ipRateLimitKeyFromRequest(req) {
805
816
  }
806
817
 
807
818
  // src/middleware/rate-limit.ts
808
- var createIpRateLimiter = () => {
809
- const maxRequests = getRateLimitMax();
819
+ var STORE_ERROR_LOG_INTERVAL_MS = 6e4;
820
+ var lastStoreErrorLoggedAt = 0;
821
+ var suppressedStoreErrors = 0;
822
+ function noteRateLimitStoreError(error) {
823
+ suppressedStoreErrors += 1;
824
+ const now = Date.now();
825
+ if (now - lastStoreErrorLoggedAt < STORE_ERROR_LOG_INTERVAL_MS) return;
826
+ lastStoreErrorLoggedAt = now;
827
+ const suppressed = suppressedStoreErrors;
828
+ suppressedStoreErrors = 0;
829
+ secureLogger.error(
830
+ "[rate-limit] Redis store error \u2014 requests are passing UNLIMITED until it recovers",
831
+ {
832
+ errorMessage: error instanceof Error ? error.message : String(error),
833
+ occurrencesSinceLastLog: suppressed
834
+ }
835
+ );
836
+ }
837
+ function isRateLimiterDegraded() {
838
+ return lastStoreErrorLoggedAt > 0 && Date.now() - lastStoreErrorLoggedAt < STORE_ERROR_LOG_INTERVAL_MS * 2;
839
+ }
840
+ function hasValidMonitorSecret(req) {
841
+ const secret = getEnvVar("MONITOR_SECRET");
842
+ const provided = req.headers["x-monitor-secret"];
843
+ if (!secret || typeof provided !== "string") return false;
844
+ const a = Buffer.from(provided);
845
+ const b = Buffer.from(secret);
846
+ return a.length === b.length && timingSafeEqual(a, b);
847
+ }
848
+ function hasCredential(req) {
849
+ if (typeof req.headers["x-api-key"] === "string") return true;
850
+ const auth = req.headers.authorization;
851
+ return typeof auth === "string" && auth.startsWith("Bearer ");
852
+ }
853
+ var createIpRateLimiter = (opts = {}) => {
854
+ const lane = opts.lane ?? "anonymous";
855
+ const maxRequests = lane === "credentialed" ? getCredentialedRateLimitMax() : getRateLimitMax();
810
856
  const redisClient = getRedisClient();
811
857
  const isProduction2 = getNodeEnv() === "production";
812
858
  if (isProduction2 && !redisClient) {
@@ -824,11 +870,11 @@ var createIpRateLimiter = () => {
824
870
  }
825
871
  const storeConfig = redisClient ? {
826
872
  store: new RedisStore({
827
- sendCommand: ((...args) => redisClient.call(
828
- args[0],
829
- ...args.slice(1)
830
- )),
831
- prefix: "rl:ip:"
873
+ sendCommand: ((...args) => redisClient.call(args[0], ...args.slice(1)).catch((error) => {
874
+ noteRateLimitStoreError(error);
875
+ throw error;
876
+ })),
877
+ prefix: lane === "credentialed" ? "rl:ip:cred:" : "rl:ip:"
832
878
  })
833
879
  } : {};
834
880
  return rateLimit({
@@ -837,14 +883,15 @@ var createIpRateLimiter = () => {
837
883
  max: maxRequests,
838
884
  standardHeaders: true,
839
885
  legacyHeaders: false,
886
+ // Availability over enforcement. A Redis outage or an exhausted provider
887
+ // quota must degrade to unlimited traffic, never to a 500 on every route.
888
+ // On 2026-08-19 an Upstash quota exhaustion took the whole service down
889
+ // for ~16h because the store error propagated to Express.
890
+ passOnStoreError: true,
840
891
  keyGenerator: (req) => ipRateLimitKeyFromRequest(req),
841
892
  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);
893
+ if (hasValidMonitorSecret(req)) return true;
894
+ return lane === "credentialed" ? !hasCredential(req) : hasCredential(req);
848
895
  },
849
896
  handler: (_req, res) => {
850
897
  res.setHeader("Retry-After", "60");
@@ -858,6 +905,24 @@ var createIpRateLimiter = () => {
858
905
  }
859
906
  });
860
907
  };
908
+ var createLocalRateLimiter = (maxPerMinute = 120) => rateLimit({
909
+ windowMs: 60 * 1e3,
910
+ max: maxPerMinute,
911
+ standardHeaders: true,
912
+ legacyHeaders: false,
913
+ keyGenerator: (req) => ipRateLimitKeyFromRequest(req),
914
+ skip: hasValidMonitorSecret,
915
+ handler: (_req, res) => {
916
+ res.setHeader("Retry-After", "60");
917
+ res.status(429).json(
918
+ apiError(
919
+ "rate_limit_error",
920
+ "RATE_LIMITED",
921
+ `Rate limit exceeded. Maximum ${maxPerMinute} requests per minute.`
922
+ )
923
+ );
924
+ }
925
+ });
861
926
 
862
927
  // src/health/index.ts
863
928
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
@@ -1521,8 +1586,16 @@ var PUBLIC_STATUS_DIMENSIONS = [
1521
1586
  "taskCanaries",
1522
1587
  "workflowRouting",
1523
1588
  "mcpProtocol",
1524
- "catalogIntegrity"
1589
+ "catalogIntegrity",
1590
+ "rateLimiter"
1525
1591
  ];
1592
+ function readRateLimiterSignal() {
1593
+ return {
1594
+ configured: isRedisConfigured(),
1595
+ available: isRedisAvailable(),
1596
+ degraded: isRateLimiterDegraded()
1597
+ };
1598
+ }
1526
1599
  var CATALOG_CHECK_NAMES = [
1527
1600
  "mcp_initialize",
1528
1601
  "tools_list_root_surface",
@@ -1646,6 +1719,56 @@ function buildCatalogIntegrityDimension(history) {
1646
1719
  }
1647
1720
  };
1648
1721
  }
1722
+ var RATE_LIMITER_SCOPE = "This instance's shared rate-limit store, upstream response cache, and idempotency keys.";
1723
+ function buildRateLimiterDimension(signal) {
1724
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
1725
+ if (!signal.configured) {
1726
+ return {
1727
+ status: "healthy",
1728
+ scope: RATE_LIMITER_SCOPE,
1729
+ evidence: {
1730
+ state: "absent",
1731
+ source: "serving_instance_redis",
1732
+ observedAt,
1733
+ message: "No REDIS_URL on this instance, so limits, cache and idempotency are per-instance and in memory by design."
1734
+ }
1735
+ };
1736
+ }
1737
+ if (!signal.available) {
1738
+ return {
1739
+ status: "unhealthy",
1740
+ scope: RATE_LIMITER_SCOPE,
1741
+ evidence: {
1742
+ state: "storage_unavailable",
1743
+ source: "serving_instance_redis",
1744
+ observedAt,
1745
+ 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."
1746
+ }
1747
+ };
1748
+ }
1749
+ if (signal.degraded) {
1750
+ return {
1751
+ status: "degraded",
1752
+ scope: RATE_LIMITER_SCOPE,
1753
+ evidence: {
1754
+ state: "observed",
1755
+ source: "serving_instance_redis",
1756
+ observedAt,
1757
+ message: "The rate-limit store returned errors recently; those requests failed open and were not counted."
1758
+ }
1759
+ };
1760
+ }
1761
+ return {
1762
+ status: "healthy",
1763
+ scope: RATE_LIMITER_SCOPE,
1764
+ evidence: {
1765
+ state: "observed",
1766
+ source: "serving_instance_redis",
1767
+ observedAt,
1768
+ message: "This instance holds a live connection to the rate-limit store."
1769
+ }
1770
+ };
1771
+ }
1649
1772
  function overallStatus(dimensions) {
1650
1773
  const required = [
1651
1774
  dimensions.providers,
@@ -1653,7 +1776,8 @@ function overallStatus(dimensions) {
1653
1776
  dimensions.workflowRouting,
1654
1777
  dimensions.mcpProtocol,
1655
1778
  dimensions.oauthLifecycle,
1656
- dimensions.catalogIntegrity
1779
+ dimensions.catalogIntegrity,
1780
+ dimensions.rateLimiter
1657
1781
  ];
1658
1782
  if (required.some((dimension) => dimension.status === "unhealthy")) {
1659
1783
  return "unhealthy";
@@ -1666,7 +1790,7 @@ function overallStatus(dimensions) {
1666
1790
  }
1667
1791
  return "healthy";
1668
1792
  }
1669
- function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, protocol, oauthLifecycle) {
1793
+ function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, protocol, oauthLifecycle, rateLimiter = readRateLimiterSignal()) {
1670
1794
  const results = providers.latest ?? {
1671
1795
  status: "unknown",
1672
1796
  totalApis: 0,
@@ -1725,7 +1849,8 @@ function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, pro
1725
1849
  },
1726
1850
  mcpProtocol: buildMcpProtocolDimension(protocol),
1727
1851
  oauthLifecycle: buildOAuthLifecycleDimension(oauthLifecycle),
1728
- catalogIntegrity: buildCatalogIntegrityDimension(protocol)
1852
+ catalogIntegrity: buildCatalogIntegrityDimension(protocol),
1853
+ rateLimiter: buildRateLimiterDimension(rateLimiter)
1729
1854
  };
1730
1855
  return {
1731
1856
  ...results,
@@ -1749,7 +1874,8 @@ function buildPublicHealthSnapshot(providers, taskCanaries, workflowRouting, pro
1749
1874
  "workflowRouting",
1750
1875
  "mcpProtocol",
1751
1876
  "oauthLifecycle",
1752
- "catalogIntegrity"
1877
+ "catalogIntegrity",
1878
+ "rateLimiter"
1753
1879
  ],
1754
1880
  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
1881
  }
@@ -2353,8 +2479,91 @@ var getCorsConfig = () => {
2353
2479
 
2354
2480
  // src/middleware/api-key.ts
2355
2481
  import { createHash, randomUUID } from "crypto";
2482
+
2483
+ // src/services/user-rate-limit.ts
2484
+ var WINDOW_MS = 6e4;
2485
+ var WINDOW_SECONDS = 60;
2486
+ var PLAN_CACHE_TTL_MS = 6e4;
2487
+ var planCache = /* @__PURE__ */ new Map();
2488
+ var FALLBACK_LIMIT_PER_MIN = 100;
2489
+ var localCounters = /* @__PURE__ */ new Map();
2490
+ async function resolveUserRateLimit(userId) {
2491
+ const cached = planCache.get(userId);
2492
+ if (cached && cached.expiresAt > Date.now()) return cached.limit;
2493
+ let limit = FALLBACK_LIMIT_PER_MIN;
2494
+ try {
2495
+ const supabase = getSupabaseAdmin();
2496
+ const { data: profile } = await supabase.from("user_profiles").select("plan_id").eq("id", userId).single();
2497
+ const planId = profile?.plan_id ?? "demo";
2498
+ const { data: plan } = await supabase.from("plans").select("rate_limit_per_min").eq("id", planId).single();
2499
+ if (typeof plan?.rate_limit_per_min === "number") {
2500
+ limit = plan.rate_limit_per_min;
2501
+ }
2502
+ } catch (error) {
2503
+ secureLogger.warn("Plan rate lookup failed \u2014 using the free-tier rate", {
2504
+ errorMessage: error instanceof Error ? error.message : String(error)
2505
+ });
2506
+ }
2507
+ planCache.set(userId, { limit, expiresAt: Date.now() + PLAN_CACHE_TTL_MS });
2508
+ return limit;
2509
+ }
2510
+ function consumeLocal(key, limit) {
2511
+ const now = Date.now();
2512
+ const existing = localCounters.get(key);
2513
+ const entry = existing && existing.expiresAt > now ? existing : { count: 0, expiresAt: now + WINDOW_MS };
2514
+ entry.count += 1;
2515
+ localCounters.set(key, entry);
2516
+ const resetSeconds = Math.max(1, Math.ceil((entry.expiresAt - now) / 1e3));
2517
+ return {
2518
+ allowed: entry.count <= limit,
2519
+ limit,
2520
+ remaining: Math.max(0, limit - entry.count),
2521
+ resetSeconds,
2522
+ degraded: false
2523
+ };
2524
+ }
2525
+ async function consumeUserRateLimit(userId, limitPerMin) {
2526
+ const window = Math.floor(Date.now() / WINDOW_MS);
2527
+ const key = `rl:user:${userId}:${window}`;
2528
+ const redis = getRedisClient();
2529
+ if (!redis) return consumeLocal(key, limitPerMin);
2530
+ try {
2531
+ const count = await redis.incr(key);
2532
+ if (count === 1) await redis.expire(key, WINDOW_SECONDS);
2533
+ const resetSeconds = Math.max(
2534
+ 1,
2535
+ WINDOW_SECONDS - Math.floor(Date.now() % WINDOW_MS / 1e3)
2536
+ );
2537
+ return {
2538
+ allowed: count <= limitPerMin,
2539
+ limit: limitPerMin,
2540
+ remaining: Math.max(0, limitPerMin - count),
2541
+ resetSeconds,
2542
+ degraded: false
2543
+ };
2544
+ } catch (error) {
2545
+ secureLogger.warn(
2546
+ "[user-rate-limit] store error \u2014 request passed UNLIMITED",
2547
+ { errorMessage: error instanceof Error ? error.message : String(error) }
2548
+ );
2549
+ return {
2550
+ allowed: true,
2551
+ limit: limitPerMin,
2552
+ remaining: limitPerMin,
2553
+ resetSeconds: WINDOW_SECONDS,
2554
+ degraded: true
2555
+ };
2556
+ }
2557
+ }
2558
+ async function enforceUserRateLimit(userId) {
2559
+ const limit = await resolveUserRateLimit(userId);
2560
+ return consumeUserRateLimit(userId, limit);
2561
+ }
2562
+
2563
+ // src/middleware/api-key.ts
2356
2564
  var REQUEST_COST = 1;
2357
- function attachVerifiedContext(req, res, key, result, quotaRemaining) {
2565
+ 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.";
2566
+ function attachVerifiedContext(req, res, key, result, quotaRemaining, rateDecision) {
2358
2567
  const authReq = req;
2359
2568
  authReq.apiKey = key;
2360
2569
  authReq.apiKeyEnvironment = getKeyEnvironment(key);
@@ -2362,7 +2571,7 @@ function attachVerifiedContext(req, res, key, result, quotaRemaining) {
2362
2571
  authReq.planId = result.planId;
2363
2572
  authReq.unkeyKeyId = result.keyId;
2364
2573
  authReq.quotaRemaining = quotaRemaining ?? void 0;
2365
- authReq.ratelimitRemaining = result.ratelimit?.remaining;
2574
+ authReq.ratelimitRemaining = rateDecision?.remaining ?? result.ratelimit?.remaining;
2366
2575
  authReq.unkeyResult = result;
2367
2576
  if (quotaRemaining !== void 0) {
2368
2577
  res.setHeader(
@@ -2370,9 +2579,11 @@ function attachVerifiedContext(req, res, key, result, quotaRemaining) {
2370
2579
  quotaRemaining === null ? "unlimited" : String(quotaRemaining)
2371
2580
  );
2372
2581
  }
2373
- if (result.ratelimit) {
2374
- res.setHeader("X-RateLimit-Remaining", String(result.ratelimit.remaining));
2375
- res.setHeader("X-RateLimit-Limit", String(result.ratelimit.limit));
2582
+ const limit = rateDecision?.limit ?? result.ratelimit?.limit;
2583
+ const remaining = rateDecision?.remaining ?? result.ratelimit?.remaining;
2584
+ if (limit !== void 0 && remaining !== void 0) {
2585
+ res.setHeader("X-RateLimit-Remaining", String(remaining));
2586
+ res.setHeader("X-RateLimit-Limit", String(limit));
2376
2587
  }
2377
2588
  }
2378
2589
  function deferApiKeyCreditDebit(req, _res, next) {
@@ -2397,7 +2608,7 @@ async function debitApiKeyCreditForExecution(req, res) {
2397
2608
  return {
2398
2609
  type: "rate_limit_error",
2399
2610
  code: "QUOTA_EXCEEDED",
2400
- message: "Monthly credit quota exhausted. Upgrade your plan or wait for the next cycle.",
2611
+ message: REST_QUOTA_EXHAUSTED_MESSAGE,
2401
2612
  runtimeStatus: "rate_limited"
2402
2613
  };
2403
2614
  }
@@ -2529,8 +2740,25 @@ function requireApiKey(req, res, next) {
2529
2740
  );
2530
2741
  return;
2531
2742
  }
2743
+ const rateDecision = await enforceUserRateLimit(result.userId);
2744
+ res.setHeader("RateLimit-Limit", String(rateDecision.limit));
2745
+ res.setHeader("RateLimit-Remaining", String(rateDecision.remaining));
2746
+ res.setHeader("RateLimit-Reset", String(rateDecision.resetSeconds));
2747
+ res.setHeader("RateLimit-Policy", `${rateDecision.limit};w=60`);
2748
+ if (!rateDecision.allowed) {
2749
+ res.setHeader("Retry-After", String(rateDecision.resetSeconds));
2750
+ res.status(429).json(
2751
+ apiError(
2752
+ "rate_limit_error",
2753
+ "RATE_LIMITED",
2754
+ `Rate limit exceeded: ${rateDecision.limit} requests per minute on your plan. This limit is shared across all of your API keys and connected clients.`,
2755
+ { requestId: req.requestId }
2756
+ )
2757
+ );
2758
+ return;
2759
+ }
2532
2760
  if (req.deferCreditDebit) {
2533
- attachVerifiedContext(req, res, key, result);
2761
+ attachVerifiedContext(req, res, key, result, void 0, rateDecision);
2534
2762
  next();
2535
2763
  return;
2536
2764
  }
@@ -2552,7 +2780,7 @@ function requireApiKey(req, res, next) {
2552
2780
  apiError(
2553
2781
  "rate_limit_error",
2554
2782
  "QUOTA_EXCEEDED",
2555
- "Monthly credit quota exhausted. Upgrade your plan or wait for the next cycle.",
2783
+ REST_QUOTA_EXHAUSTED_MESSAGE,
2556
2784
  { requestId: req.requestId }
2557
2785
  )
2558
2786
  );
@@ -2573,7 +2801,14 @@ function requireApiKey(req, res, next) {
2573
2801
  );
2574
2802
  return;
2575
2803
  }
2576
- attachVerifiedContext(req, res, key, result, consumeResult.balance);
2804
+ attachVerifiedContext(
2805
+ req,
2806
+ res,
2807
+ key,
2808
+ result,
2809
+ consumeResult.balance,
2810
+ rateDecision
2811
+ );
2577
2812
  next();
2578
2813
  } catch (err) {
2579
2814
  secureLogger.error("consume_credits threw \u2014 rejecting request", err, {
@@ -2666,9 +2901,6 @@ function requireApiKey(req, res, next) {
2666
2901
 
2667
2902
  // src/routes/auth.ts
2668
2903
  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
2904
 
2673
2905
  // src/middleware/supabase-auth.ts
2674
2906
  function requireSupabaseAuth(req, res, next) {
@@ -2728,136 +2960,6 @@ var asyncHandler = (fn) => (req, res, next) => {
2728
2960
 
2729
2961
  // src/routes/auth.ts
2730
2962
  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
2963
  router2.get(
2862
2964
  "/me",
2863
2965
  requireSupabaseAuth,
@@ -2875,7 +2977,7 @@ var auth_default = router2;
2875
2977
 
2876
2978
  // src/routes/keys.ts
2877
2979
  import { Router as Router3 } from "express";
2878
- import { z as z2 } from "zod";
2980
+ import { z } from "zod";
2879
2981
 
2880
2982
  // src/middleware/idempotency.ts
2881
2983
  import { createHash as createHash2 } from "crypto";
@@ -3092,7 +3194,7 @@ function replayResponse(res, cached) {
3092
3194
  error: {
3093
3195
  type: "invalid_request_error",
3094
3196
  code: "SECRET_ALREADY_ISSUED",
3095
- message: "This request was already completed and its one-time secret was returned only in the original response. It cannot be retrieved again \u2014 create a new key if you no longer have it."
3197
+ message: "This request was already completed and its one-time secret was returned only in the original response. It cannot be retrieved again; create a new key if you no longer have it."
3096
3198
  }
3097
3199
  });
3098
3200
  return;
@@ -3208,9 +3310,9 @@ async function handleRedisLookup(key, requestFingerprint, req, res, next) {
3208
3310
 
3209
3311
  // src/routes/keys.ts
3210
3312
  var router3 = Router3();
3211
- var CreateKeySchema = z2.object({
3212
- name: z2.string().min(1).max(100).optional(),
3213
- b2b_subjects_enabled: z2.boolean().optional()
3313
+ var CreateKeySchema = z.object({
3314
+ name: z.string().min(1).max(100).optional(),
3315
+ b2b_subjects_enabled: z.boolean().optional()
3214
3316
  });
3215
3317
  router3.post(
3216
3318
  "/",
@@ -3248,7 +3350,7 @@ router3.post(
3248
3350
  plan: planId,
3249
3351
  b2b_subjects_enabled: b2b_subjects_enabled ?? false,
3250
3352
  ...result.subjectSigningSecret ? { subject_signing_secret: result.subjectSigningSecret } : {},
3251
- warning: "Save this key \u2014 it will not be shown again."
3353
+ warning: "Save this key; it will not be shown again."
3252
3354
  }
3253
3355
  });
3254
3356
  } catch (err) {
@@ -3375,6 +3477,7 @@ router4.get(
3375
3477
  const { data: plan } = await getSupabaseAdmin().from("plans").select("*").eq("id", resolvedPlanId).single();
3376
3478
  await ensureWallet(userId, resolvedPlanId);
3377
3479
  const wallet = await getWalletStatus(userId);
3480
+ 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
3481
  const keys = await listKeys(userId);
3379
3482
  const keyEntries = keys.map((k) => ({
3380
3483
  key_id: k.keyId,
@@ -3398,6 +3501,11 @@ router4.get(
3398
3501
  cycle_start: wallet.cycleStart,
3399
3502
  cycle_end: wallet.cycleEnd
3400
3503
  } : null,
3504
+ subscription: subscription ? {
3505
+ status: subscription.status,
3506
+ current_period_start: subscription.current_period_start,
3507
+ current_period_end: subscription.current_period_end
3508
+ } : null,
3401
3509
  keys: keyEntries,
3402
3510
  total_keys: keys.length
3403
3511
  }
@@ -3588,13 +3696,13 @@ var b2b_default = router5;
3588
3696
  import {
3589
3697
  Router as Router6
3590
3698
  } from "express";
3591
- import { z as z3 } from "zod";
3699
+ import { z as z2 } from "zod";
3592
3700
 
3593
3701
  // src/services/stripe-service.ts
3594
3702
  import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
3595
3703
  var STRIPE_API_BASE = "https://api.stripe.com";
3596
3704
  var STRIPE_SIGNATURE_TOLERANCE_SECONDS = 300;
3597
- var PAID_PLAN_ID = "pro";
3705
+ var PAID_PLAN_ID = "analyst";
3598
3706
  var FREE_PLAN_ID = "demo";
3599
3707
  function getStripeSecretKey() {
3600
3708
  return getEnvVar("STRIPE_SECRET_KEY");
@@ -3605,16 +3713,44 @@ function getStripeWebhookSecret() {
3605
3713
  function isStripeCheckoutConfigured() {
3606
3714
  return Boolean(getStripeSecretKey());
3607
3715
  }
3608
- function priceIdForCycle(cycle) {
3609
- return cycle === "yearly" ? getEnvVar("STRIPE_PRICE_PRO_YEARLY") : getEnvVar("STRIPE_PRICE_PRO_MONTHLY");
3716
+ var STRIPE_PRICE_ENV = {
3717
+ starter: {
3718
+ monthly: "STRIPE_PRICE_STARTER_MONTHLY",
3719
+ yearly: "STRIPE_PRICE_STARTER_YEARLY"
3720
+ },
3721
+ analyst: {
3722
+ monthly: "STRIPE_PRICE_PRO_MONTHLY",
3723
+ yearly: "STRIPE_PRICE_PRO_YEARLY"
3724
+ }
3725
+ };
3726
+ function priceIdForPlanCycle(planId, cycle) {
3727
+ const row = STRIPE_PRICE_ENV[planId];
3728
+ if (!row) return void 0;
3729
+ return getEnvVar(row[cycle]);
3610
3730
  }
3611
3731
  function planIdForPrice(priceId) {
3612
3732
  if (!priceId) return null;
3613
- if (priceId === getEnvVar("STRIPE_PRICE_PRO_MONTHLY") || priceId === getEnvVar("STRIPE_PRICE_PRO_YEARLY")) {
3614
- return PAID_PLAN_ID;
3733
+ for (const [planId, cycles] of Object.entries(STRIPE_PRICE_ENV)) {
3734
+ for (const envName of Object.values(cycles)) {
3735
+ if (priceId === getEnvVar(envName)) return planId;
3736
+ }
3615
3737
  }
3616
3738
  return null;
3617
3739
  }
3740
+ var STRIPE_SUBSCRIPTION_STATUSES = [
3741
+ "incomplete",
3742
+ "incomplete_expired",
3743
+ "trialing",
3744
+ "active",
3745
+ "past_due",
3746
+ "canceled",
3747
+ "unpaid",
3748
+ "paused"
3749
+ ];
3750
+ function registrySubscriptionStatus(status) {
3751
+ if (!status) return null;
3752
+ return STRIPE_SUBSCRIPTION_STATUSES.includes(status) ? status : null;
3753
+ }
3618
3754
  function getBillingSuccessUrl() {
3619
3755
  return getEnvVar("BILLING_SUCCESS_URL") ?? "https://www.hiveintelligence.xyz/dashboard?checkout=success";
3620
3756
  }
@@ -3734,6 +3870,164 @@ async function createStripeCheckoutSession(args) {
3734
3870
  return { id, url };
3735
3871
  }
3736
3872
 
3873
+ // src/services/llamapay-service.ts
3874
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual4 } from "crypto";
3875
+ var LLAMAPAY_API_BASE = "https://api.llamapay.io";
3876
+ var LLAMAPAY_SIGNATURE_HEADER = "x-cc-webhook-signature";
3877
+ function getLlamaPayApiKey() {
3878
+ const key = process.env.LLAMAPAY_API_KEY?.trim();
3879
+ return key && key.length > 0 ? key : void 0;
3880
+ }
3881
+ function getLlamaPayWebhookSecret() {
3882
+ const secret = process.env.LLAMAPAY_WEBHOOK_SECRET?.trim();
3883
+ return secret && secret.length > 0 ? secret : void 0;
3884
+ }
3885
+ function isLlamaPayCheckoutConfigured() {
3886
+ return getLlamaPayApiKey() !== void 0;
3887
+ }
3888
+ var PLAN_PRICES_USD = {
3889
+ starter: { monthly: 49, yearly: 490 },
3890
+ analyst: { monthly: 149, yearly: 1490 }
3891
+ };
3892
+ var PLAN_TIER_ORDER = [
3893
+ "demo",
3894
+ "starter",
3895
+ "analyst",
3896
+ "enterprise"
3897
+ ];
3898
+ function isSellablePlan(planId) {
3899
+ return Object.prototype.hasOwnProperty.call(PLAN_PRICES_USD, planId);
3900
+ }
3901
+ function priceForPlanCycle(planId, cycle) {
3902
+ const row = PLAN_PRICES_USD[planId];
3903
+ if (!row) return void 0;
3904
+ return cycle === "yearly" ? row.yearly : row.monthly;
3905
+ }
3906
+ function canUpgradeTo(current, target) {
3907
+ const currentIndex = PLAN_TIER_ORDER.indexOf(current ?? "demo");
3908
+ const targetIndex = PLAN_TIER_ORDER.indexOf(target);
3909
+ if (targetIndex < 0) return false;
3910
+ return targetIndex > (currentIndex < 0 ? 0 : currentIndex);
3911
+ }
3912
+ var RENEWAL_WINDOW_DAYS = 30;
3913
+ function canPurchasePlan(current, target, termEnd, now = /* @__PURE__ */ new Date()) {
3914
+ if (canUpgradeTo(current, target)) return true;
3915
+ if (current !== target) return false;
3916
+ if (!termEnd) return false;
3917
+ const end = termEnd instanceof Date ? termEnd : new Date(termEnd);
3918
+ if (Number.isNaN(end.getTime())) return false;
3919
+ const windowOpens = new Date(
3920
+ end.getTime() - RENEWAL_WINDOW_DAYS * 24 * 60 * 60 * 1e3
3921
+ );
3922
+ return now.getTime() >= windowOpens.getTime();
3923
+ }
3924
+ function pricingTypeFor(cycle) {
3925
+ return cycle === "yearly" ? "fixed_price" : "subscription";
3926
+ }
3927
+ function addUtcMonths(start, months) {
3928
+ const end = new Date(start.getTime());
3929
+ const day = end.getUTCDate();
3930
+ end.setUTCDate(1);
3931
+ end.setUTCMonth(end.getUTCMonth() + months);
3932
+ const daysInTargetMonth = new Date(
3933
+ Date.UTC(end.getUTCFullYear(), end.getUTCMonth() + 1, 0)
3934
+ ).getUTCDate();
3935
+ end.setUTCDate(Math.min(day, daysInTargetMonth));
3936
+ return end;
3937
+ }
3938
+ function creditCycleEndFrom(start) {
3939
+ return addUtcMonths(start, 1);
3940
+ }
3941
+ function termEndFrom(start, cycle) {
3942
+ return addUtcMonths(start, cycle === "yearly" ? 12 : 1);
3943
+ }
3944
+ var LlamaPayApiError = class extends Error {
3945
+ constructor(status, message) {
3946
+ super(message);
3947
+ this.status = status;
3948
+ this.name = "LlamaPayApiError";
3949
+ }
3950
+ status;
3951
+ };
3952
+ async function createLlamaPayCharge(args) {
3953
+ const apiKey = getLlamaPayApiKey();
3954
+ if (!apiKey) {
3955
+ throw new LlamaPayApiError(503, "LlamaPay API key is not configured");
3956
+ }
3957
+ let response;
3958
+ try {
3959
+ response = await fetch(`${LLAMAPAY_API_BASE}/charges`, {
3960
+ method: "POST",
3961
+ headers: {
3962
+ Authorization: apiKey,
3963
+ "Content-Type": "application/json"
3964
+ },
3965
+ body: JSON.stringify({
3966
+ pricing_type: pricingTypeFor(args.cycle),
3967
+ local_price: {
3968
+ amount: args.amountUsd.toFixed(2),
3969
+ currency: "USD"
3970
+ },
3971
+ metadata: {
3972
+ user_id: args.userId,
3973
+ plan_id: args.planId,
3974
+ billing_cycle: args.cycle,
3975
+ ...args.email ? { email: args.email } : {}
3976
+ },
3977
+ redirect_url: getBillingSuccessUrl(),
3978
+ cancel_url: getBillingCancelUrl()
3979
+ })
3980
+ });
3981
+ } catch (err) {
3982
+ throw new LlamaPayApiError(
3983
+ 502,
3984
+ `LlamaPay request failed: ${err instanceof Error ? err.message : "unknown"}`
3985
+ );
3986
+ }
3987
+ const text = await response.text();
3988
+ if (!response.ok) {
3989
+ throw new LlamaPayApiError(
3990
+ response.status,
3991
+ `LlamaPay charge creation failed (${response.status}): ${text.slice(0, 300)}`
3992
+ );
3993
+ }
3994
+ let payload;
3995
+ try {
3996
+ payload = JSON.parse(text);
3997
+ } catch {
3998
+ throw new LlamaPayApiError(502, "LlamaPay returned a non-JSON response");
3999
+ }
4000
+ const id = payload.data?.id;
4001
+ const hostedUrl = payload.data?.hosted_url;
4002
+ if (typeof id !== "string" || typeof hostedUrl !== "string" || !hostedUrl) {
4003
+ throw new LlamaPayApiError(
4004
+ 502,
4005
+ "LlamaPay charge creation returned no hosted_url"
4006
+ );
4007
+ }
4008
+ return { id, hostedUrl };
4009
+ }
4010
+ function verifyLlamaPaySignature(args) {
4011
+ const header = args.header;
4012
+ if (typeof header !== "string" || header.length === 0) {
4013
+ return { valid: false, reason: "missing_header" };
4014
+ }
4015
+ const received = header.trim().toLowerCase();
4016
+ if (!/^[0-9a-f]+$/.test(received)) {
4017
+ return { valid: false, reason: "malformed_header" };
4018
+ }
4019
+ const expected = createHmac2("sha256", args.secret).update(args.payload).digest("hex");
4020
+ const receivedBuf = Buffer.from(received, "utf8");
4021
+ const expectedBuf = Buffer.from(expected, "utf8");
4022
+ if (receivedBuf.length !== expectedBuf.length) {
4023
+ return { valid: false, reason: "no_match" };
4024
+ }
4025
+ if (!timingSafeEqual4(receivedBuf, expectedBuf)) {
4026
+ return { valid: false, reason: "no_match" };
4027
+ }
4028
+ return { valid: true };
4029
+ }
4030
+
3737
4031
  // src/routes/billing.ts
3738
4032
  var BILLING_UNAVAILABLE = apiError(
3739
4033
  "internal_error",
@@ -3788,46 +4082,187 @@ function requireBillingPrincipal(req, res, next) {
3788
4082
  next();
3789
4083
  });
3790
4084
  }
3791
- var CheckoutSchema = z3.object({
3792
- billing_cycle: z3.enum(["monthly", "yearly"]).default("monthly")
4085
+ var CheckoutSchema = z2.object({
4086
+ // Was absent, and the route hardcoded PAID_PLAN_ID. The crypto lane sold
4087
+ // Starter and this one did not, so the two processors sold different
4088
+ // catalogues -- with Starter the emphasised middle card on /pricing.
4089
+ plan_id: z2.string().min(1).default(PAID_PLAN_ID),
4090
+ billing_cycle: z2.enum(["monthly", "yearly"]).default("monthly")
3793
4091
  });
3794
4092
  var OCCUPYING_BILLING_STATUSES = /* @__PURE__ */ new Set(["active", "past_due", "unpaid"]);
3795
- var billingRouter = Router6();
3796
- billingRouter.post(
3797
- "/checkout-session",
3798
- requireBillingPrincipal,
3799
- asyncHandler(async (req, res) => {
3800
- const userId = req.billingUserId;
3801
- if (!isStripeCheckoutConfigured()) {
3802
- return res.status(503).json(BILLING_UNAVAILABLE);
3803
- }
3804
- if (!isSupabaseConfigured()) {
3805
- return res.status(503).json(BILLING_UNAVAILABLE);
3806
- }
3807
- const parsed = CheckoutSchema.safeParse(req.body ?? {});
3808
- if (!parsed.success) {
3809
- return res.status(400).json(
3810
- apiError(
3811
- "invalid_request_error",
3812
- "VALIDATION_ERROR",
3813
- 'billing_cycle must be "monthly" or "yearly".'
3814
- )
4093
+ var CHECKOUT_LOCK_TTL_MS = 6e4;
4094
+ var CHECKOUT_LOCK_PREFIX = "billing:checkout:";
4095
+ var localCheckoutLocks = /* @__PURE__ */ new Map();
4096
+ async function acquireCheckoutLock(userId) {
4097
+ const key = `${CHECKOUT_LOCK_PREFIX}${userId}`;
4098
+ const redis = isRedisAvailable() ? getRedisClient() : null;
4099
+ if (redis) {
4100
+ try {
4101
+ const claimed = await redis.set(
4102
+ key,
4103
+ "1",
4104
+ "PX",
4105
+ CHECKOUT_LOCK_TTL_MS,
4106
+ "NX"
3815
4107
  );
4108
+ return claimed === "OK";
4109
+ } catch (err) {
4110
+ secureLogger.warn("Checkout lock fell back to process memory", {
4111
+ reason: err instanceof Error ? err.message : "unknown"
4112
+ });
3816
4113
  }
3817
- const priceId = priceIdForCycle(parsed.data.billing_cycle);
3818
- if (!priceId) {
3819
- return res.status(503).json(BILLING_UNAVAILABLE);
3820
- }
3821
- const { data: profile } = await getSupabaseAdmin().from("user_profiles").select("billing_customer_id, billing_subscription_id, billing_status").eq("id", userId).maybeSingle();
3822
- if (profile?.billing_subscription_id && typeof profile.billing_status === "string" && OCCUPYING_BILLING_STATUSES.has(profile.billing_status)) {
3823
- return res.status(409).json(
3824
- apiError(
3825
- "invalid_request_error",
3826
- "SUBSCRIPTION_EXISTS",
3827
- "This account already has an active subscription. Next action: manage it from the dashboard billing tab instead of creating a new checkout."
3828
- )
4114
+ }
4115
+ const now = Date.now();
4116
+ const heldUntil = localCheckoutLocks.get(key);
4117
+ if (heldUntil !== void 0 && heldUntil > now) return false;
4118
+ localCheckoutLocks.set(key, now + CHECKOUT_LOCK_TTL_MS);
4119
+ return true;
4120
+ }
4121
+ async function releaseCheckoutLock(userId) {
4122
+ const key = `${CHECKOUT_LOCK_PREFIX}${userId}`;
4123
+ localCheckoutLocks.delete(key);
4124
+ const redis = isRedisAvailable() ? getRedisClient() : null;
4125
+ if (!redis) return;
4126
+ try {
4127
+ await redis.del(key);
4128
+ } catch {
4129
+ }
4130
+ }
4131
+ var CHECKOUT_RACE_LOST = apiError(
4132
+ "invalid_request_error",
4133
+ "CHECKOUT_IN_PROGRESS",
4134
+ "Another checkout request for this account is still being created. Next action: wait a couple of seconds and retry; two simultaneous requests can only ever produce one charge."
4135
+ );
4136
+ function checkoutAlreadyOpen(chargeId) {
4137
+ return apiError(
4138
+ "invalid_request_error",
4139
+ "CHECKOUT_IN_PROGRESS",
4140
+ `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; otherwise it clears on its own ${OPEN_CHARGE_WINDOW_MINUTES} minutes after the page was opened.`
4141
+ );
4142
+ }
4143
+ function withCheckoutLock(req, res, next) {
4144
+ const userId = req.billingUserId;
4145
+ if (!userId) {
4146
+ next();
4147
+ return;
4148
+ }
4149
+ void acquireCheckoutLock(userId).then((claimed) => {
4150
+ if (!claimed) {
4151
+ res.status(409).json(CHECKOUT_RACE_LOST);
4152
+ return;
4153
+ }
4154
+ const sendJson = res.json.bind(res);
4155
+ res.json = ((body) => {
4156
+ void releaseCheckoutLock(userId);
4157
+ return sendJson(body);
4158
+ });
4159
+ next();
4160
+ });
4161
+ }
4162
+ var OPEN_CHARGE_WINDOW_MINUTES = 15;
4163
+ var OPEN_CHARGE_WINDOW_MS = OPEN_CHARGE_WINDOW_MINUTES * 60 * 1e3;
4164
+ var ABANDONED_EVENT_TYPE = "checkout:abandoned";
4165
+ function openChargeCutoff() {
4166
+ return new Date(Date.now() - OPEN_CHARGE_WINDOW_MS).toISOString();
4167
+ }
4168
+ async function findOpenCharge(userId) {
4169
+ 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);
4170
+ if (error) {
4171
+ secureLogger.warn("Open-charge lookup failed; allowing checkout", {
4172
+ userId,
4173
+ errorMessage: error.message
4174
+ });
4175
+ return null;
4176
+ }
4177
+ const row = Array.isArray(data) ? data[0] : void 0;
4178
+ return typeof row?.charge_id === "string" ? row.charge_id : null;
4179
+ }
4180
+ async function settleChargeRecord(chargeId, status, eventType) {
4181
+ if (!chargeId || !isSupabaseConfigured()) return;
4182
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4183
+ try {
4184
+ const { error } = await getSupabaseAdmin().from("billing_charges").update({
4185
+ status,
4186
+ settled_at: now,
4187
+ last_event_at: now,
4188
+ last_event_type: eventType
4189
+ }).eq("charge_id", chargeId);
4190
+ if (error) throw new Error(error.message);
4191
+ } catch (err) {
4192
+ secureLogger.error("Failed to settle billing charge record", err, {
4193
+ chargeId,
4194
+ eventType
4195
+ });
4196
+ }
4197
+ }
4198
+ async function recordChargeCreated(charge) {
4199
+ const { error } = await getSupabaseAdmin().from("billing_charges").insert({
4200
+ charge_id: charge.chargeId,
4201
+ processor: charge.processor,
4202
+ user_id: charge.userId,
4203
+ plan_id: charge.planId,
4204
+ billing_cycle: charge.cycle,
4205
+ amount_usd: charge.amountUsd,
4206
+ plan_id_before: charge.planIdBefore,
4207
+ status: "created"
4208
+ });
4209
+ if (error) {
4210
+ throw new Error(`failed to record billing charge: ${error.message}`);
4211
+ }
4212
+ }
4213
+ var billingRouter = Router6();
4214
+ billingRouter.post(
4215
+ "/checkout-session",
4216
+ requireBillingPrincipal,
4217
+ withCheckoutLock,
4218
+ asyncHandler(async (req, res) => {
4219
+ const userId = req.billingUserId;
4220
+ if (!isStripeCheckoutConfigured()) {
4221
+ return res.status(503).json(BILLING_UNAVAILABLE);
4222
+ }
4223
+ if (!isSupabaseConfigured()) {
4224
+ return res.status(503).json(BILLING_UNAVAILABLE);
4225
+ }
4226
+ const parsed = CheckoutSchema.safeParse(req.body ?? {});
4227
+ if (!parsed.success) {
4228
+ return res.status(400).json(
4229
+ apiError(
4230
+ "invalid_request_error",
4231
+ "VALIDATION_ERROR",
4232
+ 'billing_cycle must be "monthly" or "yearly".'
4233
+ )
3829
4234
  );
3830
4235
  }
4236
+ const { plan_id: planId, billing_cycle: cycle } = parsed.data;
4237
+ if (!isSellablePlan(planId)) {
4238
+ return res.status(400).json(
4239
+ apiError(
4240
+ "invalid_request_error",
4241
+ "PLAN_NOT_SELLABLE",
4242
+ `Plan "${planId}" is not sold self-serve. Next action: choose starter or ${PAID_PLAN_ID}, or contact support for Enterprise.`
4243
+ )
4244
+ );
4245
+ }
4246
+ const priceId = priceIdForPlanCycle(planId, cycle);
4247
+ if (!priceId) {
4248
+ return res.status(503).json(BILLING_UNAVAILABLE);
4249
+ }
4250
+ const { data: profile } = await getSupabaseAdmin().from("user_profiles").select(
4251
+ "plan_id, billing_customer_id, billing_subscription_id, billing_status"
4252
+ ).eq("id", userId).maybeSingle();
4253
+ if (profile?.billing_subscription_id && typeof profile.billing_status === "string" && OCCUPYING_BILLING_STATUSES.has(profile.billing_status)) {
4254
+ return res.status(409).json(
4255
+ apiError(
4256
+ "invalid_request_error",
4257
+ "SUBSCRIPTION_EXISTS",
4258
+ "This account already has an active subscription. Next action: manage it from the dashboard billing tab instead of creating a new checkout."
4259
+ )
4260
+ );
4261
+ }
4262
+ const openChargeId = await findOpenCharge(userId);
4263
+ if (openChargeId) {
4264
+ return res.status(409).json(checkoutAlreadyOpen(openChargeId));
4265
+ }
3831
4266
  try {
3832
4267
  let customerId = profile?.billing_customer_id ?? null;
3833
4268
  if (!customerId) {
@@ -3846,11 +4281,25 @@ billingRouter.post(
3846
4281
  customerId,
3847
4282
  priceId,
3848
4283
  userId,
3849
- planId: PAID_PLAN_ID
4284
+ planId
4285
+ });
4286
+ await recordChargeCreated({
4287
+ chargeId: session.id,
4288
+ processor: "stripe",
4289
+ userId,
4290
+ planId,
4291
+ cycle,
4292
+ amountUsd: priceForPlanCycle(planId, cycle) ?? 0,
4293
+ planIdBefore: asString(profile?.plan_id)
3850
4294
  });
3851
4295
  return res.json({
3852
4296
  ok: true,
3853
- data: { url: session.url, session_id: session.id }
4297
+ data: {
4298
+ url: session.url,
4299
+ session_id: session.id,
4300
+ plan_id: planId,
4301
+ billing_cycle: cycle
4302
+ }
3854
4303
  });
3855
4304
  } catch (err) {
3856
4305
  if (err instanceof StripeApiError) {
@@ -3922,176 +4371,566 @@ function invoiceSubscriptionContext(invoice) {
3922
4371
  ...metadataOf(parentDetails.metadata),
3923
4372
  ...metadataOf(details.metadata)
3924
4373
  }
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;
4374
+ };
4375
+ }
4376
+ function invoicePeriod(invoice) {
4377
+ const lines = metadataOf(invoice.lines);
4378
+ const data = Array.isArray(lines.data) ? lines.data : [];
4379
+ let best = null;
4380
+ let bestEnd = -Infinity;
4381
+ for (const raw of data) {
4382
+ const line = metadataOf(raw);
4383
+ const period2 = metadataOf(line.period);
4384
+ const end = typeof period2.end === "number" ? period2.end : null;
4385
+ if (end !== null && end > bestEnd) {
4386
+ bestEnd = end;
4387
+ best = line;
4388
+ }
4389
+ }
4390
+ if (!best) return { cycleStart: null, cycleEnd: null, priceId: null };
4391
+ const period = metadataOf(best.period);
4392
+ const price = metadataOf(best.price);
4393
+ const pricing = metadataOf(best.pricing);
4394
+ const priceDetails = metadataOf(pricing.price_details);
4395
+ return {
4396
+ cycleStart: isoFromUnixSeconds(period.start),
4397
+ cycleEnd: isoFromUnixSeconds(period.end),
4398
+ priceId: asString(price.id) ?? asString(priceDetails.price)
4399
+ };
4400
+ }
4401
+ function cappedCreditCycleEnd(cycleStart, cycleEnd) {
4402
+ if (!cycleStart) return cycleEnd;
4403
+ const start = new Date(cycleStart);
4404
+ if (Number.isNaN(start.getTime())) return cycleEnd;
4405
+ const monthly = creditCycleEndFrom(start);
4406
+ if (!cycleEnd) return monthly.toISOString();
4407
+ const end = new Date(cycleEnd);
4408
+ if (Number.isNaN(end.getTime())) return monthly.toISOString();
4409
+ return (end < monthly ? end : monthly).toISOString();
4410
+ }
4411
+ async function decideWebhookAction(event) {
4412
+ const object = metadataOf(event.data?.object);
4413
+ switch (event.type) {
4414
+ case "checkout.session.completed": {
4415
+ const mode = asString(object.mode);
4416
+ if (mode !== null && mode !== "subscription") {
4417
+ return { kind: "ignore", note: "non-subscription checkout" };
4418
+ }
4419
+ const metadata = metadataOf(object.metadata);
4420
+ const userId = asString(metadata.user_id) ?? asString(object.client_reference_id);
4421
+ if (!userId) {
4422
+ return { kind: "ignore", note: "checkout session without user_id" };
4423
+ }
4424
+ return {
4425
+ kind: "apply",
4426
+ args: {
4427
+ eventId: event.id,
4428
+ eventType: event.type,
4429
+ userId,
4430
+ newPlanId: asString(metadata.plan_id) ?? PAID_PLAN_ID,
4431
+ grantCredits: true,
4432
+ billingStatus: "active",
4433
+ customerId: refId(object.customer),
4434
+ subscriptionId: refId(object.subscription),
4435
+ subscriptionStatus: "active",
4436
+ ledgerReason: "plan_change"
4437
+ }
4438
+ };
4439
+ }
4440
+ case "customer.subscription.updated":
4441
+ case "customer.subscription.deleted": {
4442
+ const metadata = metadataOf(object.metadata);
4443
+ const customerId = refId(object.customer);
4444
+ const userId = asString(metadata.user_id) ?? await resolveUserIdByCustomer(customerId);
4445
+ if (!userId) {
4446
+ return {
4447
+ kind: "unresolved_user",
4448
+ note: `no user binding for customer ${customerId ?? "<none>"}`
4449
+ };
4450
+ }
4451
+ if (event.type === "customer.subscription.deleted") {
4452
+ return {
4453
+ kind: "apply",
4454
+ args: {
4455
+ eventId: event.id,
4456
+ eventType: event.type,
4457
+ userId,
4458
+ newPlanId: FREE_PLAN_ID,
4459
+ grantCredits: true,
4460
+ billingStatus: "canceled",
4461
+ customerId,
4462
+ subscriptionId: refId(object.id),
4463
+ subscriptionStatus: "canceled",
4464
+ ledgerReason: "plan_change"
4465
+ }
4466
+ };
4467
+ }
4468
+ const status = asString(object.status);
4469
+ const items = metadataOf(object.items);
4470
+ const firstItem = metadataOf(
4471
+ Array.isArray(items.data) ? items.data[0] : void 0
4472
+ );
4473
+ const priceId = asString(metadataOf(firstItem.price).id);
4474
+ return {
4475
+ kind: "apply",
4476
+ args: {
4477
+ eventId: event.id,
4478
+ eventType: event.type,
4479
+ userId,
4480
+ // Tier/status sync only — credits ride the money event
4481
+ // (invoice.paid), which is the one wallet clock.
4482
+ newPlanId: planIdForPrice(priceId) ?? asString(metadata.plan_id) ?? null,
4483
+ grantCredits: false,
4484
+ billingStatus: mapSubscriptionStatusToBilling(status),
4485
+ customerId,
4486
+ subscriptionId: refId(object.id),
4487
+ // Whitelisted, never forwarded raw. stripe_subscriptions.status has
4488
+ // a CHECK over eight literals; a status Stripe adds later raises
4489
+ // 23514 *inside* apply_plan_change, which rolls back the dedupe
4490
+ // claim and the wallet grant with it, 500s, and leaves Stripe
4491
+ // retrying forever. Null here means "skip the registry upsert",
4492
+ // which still applies the tier and still acks 200.
4493
+ subscriptionStatus: registrySubscriptionStatus(status),
4494
+ priceId,
4495
+ ledgerReason: "plan_change"
4496
+ }
4497
+ };
4498
+ }
4499
+ case "invoice.paid":
4500
+ case "invoice.payment_failed": {
4501
+ const { subscriptionId, metadata } = invoiceSubscriptionContext(object);
4502
+ if (!subscriptionId) {
4503
+ return { kind: "ignore", note: "invoice without a subscription" };
4504
+ }
4505
+ const customerId = refId(object.customer);
4506
+ const userId = asString(metadata.user_id) ?? await resolveUserIdByCustomer(customerId);
4507
+ if (!userId) {
4508
+ return {
4509
+ kind: "unresolved_user",
4510
+ note: `no user binding for customer ${customerId ?? "<none>"}`
4511
+ };
4512
+ }
4513
+ if (event.type === "invoice.payment_failed") {
4514
+ return {
4515
+ kind: "apply",
4516
+ args: {
4517
+ eventId: event.id,
4518
+ eventType: event.type,
4519
+ userId,
4520
+ billingStatus: "past_due",
4521
+ customerId,
4522
+ subscriptionId,
4523
+ ledgerReason: "plan_change"
4524
+ }
4525
+ };
4526
+ }
4527
+ const period = invoicePeriod(object);
4528
+ const creditCycleEnd = cappedCreditCycleEnd(
4529
+ period.cycleStart,
4530
+ period.cycleEnd
4531
+ );
4532
+ return {
4533
+ kind: "apply",
4534
+ args: {
4535
+ eventId: event.id,
4536
+ eventType: event.type,
4537
+ userId,
4538
+ newPlanId: planIdForPrice(period.priceId) ?? asString(metadata.plan_id) ?? null,
4539
+ grantCredits: true,
4540
+ cycleStart: period.cycleStart,
4541
+ cycleEnd: creditCycleEnd,
4542
+ termEnd: period.cycleEnd,
4543
+ billingStatus: "active",
4544
+ customerId,
4545
+ subscriptionId,
4546
+ priceId: period.priceId,
4547
+ ledgerReason: "refill"
4548
+ }
4549
+ };
4550
+ }
4551
+ default:
4552
+ return { kind: "ignore", note: `unhandled event type ${event.type}` };
4553
+ }
4554
+ }
4555
+ var stripeWebhookHandler = asyncHandler(
4556
+ async (req, res) => {
4557
+ const secret = getStripeWebhookSecret();
4558
+ if (!secret) {
4559
+ return res.status(503).json(BILLING_UNAVAILABLE);
4560
+ }
4561
+ const raw = req.body;
4562
+ if (!Buffer.isBuffer(raw)) {
4563
+ secureLogger.error(
4564
+ "Stripe webhook received a non-raw body \u2014 express.raw must mount before the JSON parser",
4565
+ void 0,
4566
+ { path: req.path }
4567
+ );
4568
+ return res.status(500).json(
4569
+ apiError(
4570
+ "internal_error",
4571
+ "WEBHOOK_BODY_NOT_RAW",
4572
+ "Webhook raw body unavailable; server middleware misconfigured."
4573
+ )
4574
+ );
4575
+ }
4576
+ const verification = verifyStripeSignature({
4577
+ payload: raw,
4578
+ header: req.headers["stripe-signature"],
4579
+ secret
4580
+ });
4581
+ if (!verification.valid) {
4582
+ secureLogger.warn("Stripe webhook signature rejected", {
4583
+ reason: verification.reason
4584
+ });
4585
+ return res.status(400).json(
4586
+ apiError(
4587
+ "authentication_error",
4588
+ "INVALID_WEBHOOK_SIGNATURE",
4589
+ `Stripe signature verification failed (${verification.reason}).`
4590
+ )
4591
+ );
4592
+ }
4593
+ let event;
4594
+ try {
4595
+ event = JSON.parse(raw.toString("utf8"));
4596
+ } catch {
4597
+ return res.status(400).json(
4598
+ apiError(
4599
+ "invalid_request_error",
4600
+ "INVALID_JSON",
4601
+ "Webhook payload is not valid JSON."
4602
+ )
4603
+ );
4604
+ }
4605
+ if (!asString(event?.id) || !asString(event?.type)) {
4606
+ return res.status(400).json(
4607
+ apiError(
4608
+ "invalid_request_error",
4609
+ "INVALID_EVENT",
4610
+ "Webhook payload is missing event id or type."
4611
+ )
4612
+ );
4613
+ }
4614
+ try {
4615
+ const decision = await decideWebhookAction(event);
4616
+ if (decision.kind === "ignore") {
4617
+ return res.json({ received: true, handled: false });
4618
+ }
4619
+ if (decision.kind === "unresolved_user") {
4620
+ secureLogger.warn("Stripe webhook user unresolved \u2014 asking for retry", {
4621
+ eventType: event.type,
4622
+ note: decision.note
4623
+ });
4624
+ return res.status(500).json(
4625
+ apiError(
4626
+ "internal_error",
4627
+ "WEBHOOK_USER_UNRESOLVED",
4628
+ "Could not resolve the Hive user for this event yet; Stripe should retry."
4629
+ )
4630
+ );
4631
+ }
4632
+ const result = await applyPlanChange(decision.args);
4633
+ if (event.type === "checkout.session.completed") {
4634
+ await settleChargeRecord(
4635
+ asString(metadataOf(event.data?.object).id),
4636
+ "confirmed",
4637
+ event.type
4638
+ );
4639
+ }
4640
+ return res.json({
4641
+ received: true,
4642
+ handled: true,
4643
+ outcome: result.outcome,
4644
+ replayed: result.replayed
4645
+ });
4646
+ } catch (err) {
4647
+ secureLogger.error("Stripe webhook processing failed", err, {
4648
+ eventType: event.type
4649
+ });
4650
+ return res.status(500).json(
4651
+ apiError(
4652
+ "internal_error",
4653
+ "WEBHOOK_PROCESSING_FAILED",
4654
+ "Webhook processing failed; Stripe should retry. Processing is idempotent by event id."
4655
+ )
4656
+ );
4657
+ }
4658
+ }
4659
+ );
4660
+ var LLAMAPAY_UNAVAILABLE = apiError(
4661
+ "internal_error",
4662
+ "BILLING_UNAVAILABLE",
4663
+ "Crypto checkout is not configured on this deployment. Next action: contact support@hiveintelligence.xyz to upgrade, or retry once billing is enabled."
4664
+ );
4665
+ var CryptoCheckoutSchema = z2.object({
4666
+ plan_id: z2.string().min(1).default(PAID_PLAN_ID),
4667
+ billing_cycle: z2.enum(["monthly", "yearly"]).default("monthly")
4668
+ });
4669
+ billingRouter.post(
4670
+ "/checkout",
4671
+ requireBillingPrincipal,
4672
+ withCheckoutLock,
4673
+ asyncHandler(async (req, res) => {
4674
+ const userId = req.billingUserId;
4675
+ if (!isLlamaPayCheckoutConfigured() || !isSupabaseConfigured()) {
4676
+ return res.status(503).json(LLAMAPAY_UNAVAILABLE);
4677
+ }
4678
+ const parsed = CryptoCheckoutSchema.safeParse(req.body ?? {});
4679
+ if (!parsed.success) {
4680
+ return res.status(400).json(
4681
+ apiError(
4682
+ "invalid_request_error",
4683
+ "VALIDATION_ERROR",
4684
+ 'billing_cycle must be "monthly" or "yearly", and plan_id must be a sold plan.'
4685
+ )
4686
+ );
4687
+ }
4688
+ const { plan_id: planId, billing_cycle: cycle } = parsed.data;
4689
+ if (!isSellablePlan(planId)) {
4690
+ return res.status(400).json(
4691
+ apiError(
4692
+ "invalid_request_error",
4693
+ "PLAN_NOT_SELLABLE",
4694
+ `Plan "${planId}" is not sold self-serve. Next action: choose starter or ${PAID_PLAN_ID}, or contact support for Enterprise.`
4695
+ )
4696
+ );
4697
+ }
4698
+ const amountUsd = priceForPlanCycle(planId, cycle);
4699
+ if (amountUsd === void 0) {
4700
+ return res.status(503).json(LLAMAPAY_UNAVAILABLE);
4701
+ }
4702
+ const { data: profile } = await getSupabaseAdmin().from("user_profiles").select("plan_id, billing_subscription_id, billing_status").eq("id", userId).maybeSingle();
4703
+ const hasOccupyingSubscription = Boolean(profile?.billing_subscription_id) && typeof profile?.billing_status === "string" && OCCUPYING_BILLING_STATUSES.has(profile.billing_status);
4704
+ 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();
4705
+ if (!canPurchasePlan(
4706
+ profile?.plan_id ?? null,
4707
+ planId,
4708
+ liveSubscription?.current_period_end ?? null
4709
+ )) {
4710
+ const sameTier = profile?.plan_id === planId;
4711
+ return res.status(409).json(
4712
+ apiError(
4713
+ "invalid_request_error",
4714
+ hasOccupyingSubscription ? "SUBSCRIPTION_EXISTS" : "PLAN_NOT_AN_UPGRADE",
4715
+ 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.`
4716
+ )
4717
+ );
4718
+ }
4719
+ const openChargeId = await findOpenCharge(userId);
4720
+ if (openChargeId) {
4721
+ return res.status(409).json(checkoutAlreadyOpen(openChargeId));
4722
+ }
4723
+ try {
4724
+ const charge = await createLlamaPayCharge({
4725
+ userId,
4726
+ planId,
4727
+ cycle,
4728
+ amountUsd,
4729
+ email: req.billingUserEmail
4730
+ });
4731
+ await recordChargeCreated({
4732
+ chargeId: charge.id,
4733
+ processor: "llamapay",
4734
+ userId,
4735
+ planId,
4736
+ cycle,
4737
+ amountUsd,
4738
+ planIdBefore: asString(profile?.plan_id)
4739
+ });
4740
+ return res.json({
4741
+ ok: true,
4742
+ data: {
4743
+ url: charge.hostedUrl,
4744
+ charge_id: charge.id,
4745
+ plan_id: planId,
4746
+ billing_cycle: cycle,
4747
+ amount_usd: amountUsd,
4748
+ processor: "llamapay"
4749
+ }
4750
+ });
4751
+ } catch (err) {
4752
+ if (err instanceof LlamaPayApiError) {
4753
+ secureLogger.error("LlamaPay charge creation failed", err, {
4754
+ userId,
4755
+ status: err.status
4756
+ });
4757
+ return res.status(502).json(
4758
+ apiError(
4759
+ "provider_error",
4760
+ "BILLING_PROVIDER_ERROR",
4761
+ "LlamaPay rejected the checkout request. Next action: retry; if it persists, contact support@hiveintelligence.xyz."
4762
+ )
4763
+ );
4764
+ }
4765
+ throw err;
4766
+ }
4767
+ })
4768
+ );
4769
+ var AbandonSchema = z2.object({
4770
+ charge_id: z2.string().min(1).optional()
4771
+ });
4772
+ billingRouter.post(
4773
+ "/checkout/abandon",
4774
+ requireBillingPrincipal,
4775
+ asyncHandler(async (req, res) => {
4776
+ const userId = req.billingUserId;
4777
+ if (!isSupabaseConfigured()) {
4778
+ return res.status(503).json(BILLING_UNAVAILABLE);
4779
+ }
4780
+ const parsed = AbandonSchema.safeParse(req.body ?? {});
4781
+ if (!parsed.success) {
4782
+ return res.status(400).json(
4783
+ apiError(
4784
+ "invalid_request_error",
4785
+ "VALIDATION_ERROR",
4786
+ "charge_id must be a non-empty string when provided. Next action: omit it to release every checkout this account has open."
4787
+ )
4788
+ );
4789
+ }
4790
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4791
+ let query = getSupabaseAdmin().from("billing_charges").update({
4792
+ status: "canceled",
4793
+ last_event_at: now,
4794
+ last_event_type: ABANDONED_EVENT_TYPE
4795
+ }).eq("user_id", userId).eq("status", "created").is("settled_at", null).gte("created_at", openChargeCutoff());
4796
+ if (parsed.data.charge_id) {
4797
+ query = query.eq("charge_id", parsed.data.charge_id);
4798
+ }
4799
+ const { data, error } = await query.select("charge_id");
4800
+ if (error) {
4801
+ secureLogger.error("Failed to mark checkout abandoned", error, {
4802
+ userId
4803
+ });
4804
+ return res.status(500).json(
4805
+ apiError(
4806
+ "internal_error",
4807
+ "CHECKOUT_ABANDON_FAILED",
4808
+ `Could not release the open checkout. Next action: retry, or wait ${OPEN_CHARGE_WINDOW_MINUTES} minutes for it to clear on its own.`
4809
+ )
4810
+ );
4811
+ }
4812
+ const abandoned = (Array.isArray(data) ? data : []).map((row) => row.charge_id).filter((id) => typeof id === "string");
4813
+ return res.json({ ok: true, data: { abandoned } });
4814
+ })
4815
+ );
4816
+ billingRouter.get(
4817
+ "/charges",
4818
+ requireBillingPrincipal,
4819
+ asyncHandler(async (req, res) => {
4820
+ const userId = req.billingUserId;
4821
+ if (!isSupabaseConfigured()) {
4822
+ return res.status(503).json(BILLING_UNAVAILABLE);
4823
+ }
4824
+ const { data, error } = await getSupabaseAdmin().from("billing_charges").select(
4825
+ "charge_id, processor, plan_id, billing_cycle, amount_usd, status, created_at, settled_at"
4826
+ ).eq("user_id", userId).order("created_at", { ascending: false }).limit(50);
4827
+ if (error) {
4828
+ secureLogger.error("Failed to read billing charges", error, { userId });
4829
+ return res.status(500).json(
4830
+ apiError(
4831
+ "internal_error",
4832
+ "BILLING_HISTORY_UNAVAILABLE",
4833
+ "Could not load your billing history. Next action: retry shortly, or contact support@hiveintelligence.xyz with the date of the charge."
4834
+ )
4835
+ );
3939
4836
  }
4837
+ return res.json({ ok: true, data: { charges: data ?? [] } });
4838
+ })
4839
+ );
4840
+ var LLAMAPAY_EVENT_TYPES = [
4841
+ "charge:pending",
4842
+ "charge:confirmed",
4843
+ "subscription:expired"
4844
+ ];
4845
+ function localChargeAmountUsd(pricing) {
4846
+ const local = metadataOf(metadataOf(pricing).local);
4847
+ const currency = asString(local.currency);
4848
+ if (currency !== null && currency.toUpperCase() !== "USD") {
4849
+ return Number.NaN;
4850
+ }
4851
+ const raw = local.amount;
4852
+ const amount = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseFloat(raw) : void 0;
4853
+ if (amount === void 0 || !Number.isFinite(amount)) return void 0;
4854
+ return amount;
4855
+ }
4856
+ function decideLlamaPayAction(event) {
4857
+ const data = metadataOf(event.data);
4858
+ const metadata = metadataOf(data.metadata);
4859
+ const userId = asString(metadata.user_id);
4860
+ const chargeId = asString(data.id);
4861
+ if (!userId) {
4862
+ return { kind: "ignore", note: "charge without user_id metadata" };
3940
4863
  }
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
4864
  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" };
4865
+ case "charge:pending":
4866
+ case "charge:confirmed": {
4867
+ const planId = asString(metadata.plan_id) ?? PAID_PLAN_ID;
4868
+ const cycle = metadata.billing_cycle === "yearly" ? "yearly" : "monthly";
4869
+ const expected = priceForPlanCycle(planId, cycle);
4870
+ const paid = localChargeAmountUsd(data.pricing);
4871
+ if (expected !== void 0 && paid !== void 0 && paid !== expected) {
4872
+ return {
4873
+ kind: "ignore",
4874
+ note: `charge ${chargeId ?? "<no id>"} paid ${Number.isNaN(paid) ? "a non-USD amount" : paid} against ${expected} for ${planId}/${cycle} \u2014 entitlement withheld`
4875
+ };
3964
4876
  }
4877
+ const start = event.created_at ? new Date(event.created_at) : /* @__PURE__ */ new Date();
4878
+ const cycleStart = Number.isNaN(start.getTime()) ? /* @__PURE__ */ new Date() : start;
3965
4879
  return {
3966
4880
  kind: "apply",
3967
4881
  args: {
3968
4882
  eventId: event.id,
3969
4883
  eventType: event.type,
3970
4884
  userId,
3971
- newPlanId: asString(metadata.plan_id) ?? PAID_PLAN_ID,
4885
+ newPlanId: planId,
3972
4886
  grantCredits: true,
4887
+ cycleStart: cycleStart.toISOString(),
4888
+ // Two clocks, deliberately. Credits are sold monthly on every term,
4889
+ // so the wallet cycle is always a month; the term is what the charge
4890
+ // actually bought. Passing the annual date as the credit cycle gave
4891
+ // a $1,490 buyer one month's credits to last a year.
4892
+ cycleEnd: creditCycleEndFrom(cycleStart).toISOString(),
4893
+ termEnd: termEndFrom(cycleStart, cycle).toISOString(),
3973
4894
  billingStatus: "active",
3974
- customerId: refId(object.customer),
3975
- subscriptionId: refId(object.subscription),
4895
+ subscriptionId: chargeId,
3976
4896
  subscriptionStatus: "active",
3977
4897
  ledgerReason: "plan_change"
3978
4898
  }
3979
4899
  };
3980
4900
  }
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);
4901
+ case "subscription:expired": {
4015
4902
  return {
4016
4903
  kind: "apply",
4017
4904
  args: {
4018
4905
  eventId: event.id,
4019
4906
  eventType: event.type,
4020
4907
  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,
4908
+ newPlanId: FREE_PLAN_ID,
4909
+ grantCredits: true,
4910
+ billingStatus: "canceled",
4911
+ subscriptionId: chargeId,
4912
+ subscriptionStatus: "canceled",
4030
4913
  ledgerReason: "plan_change"
4031
4914
  }
4032
4915
  };
4033
4916
  }
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);
4917
+ default:
4063
4918
  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
- }
4919
+ kind: "ignore",
4920
+ note: `unhandled event type ${event.type} (known: ${LLAMAPAY_EVENT_TYPES.join(", ")})`
4079
4921
  };
4080
- }
4081
- default:
4082
- return { kind: "ignore", note: `unhandled event type ${event.type}` };
4083
4922
  }
4084
4923
  }
4085
- var stripeWebhookHandler = asyncHandler(
4924
+ var llamapayWebhookHandler = asyncHandler(
4086
4925
  async (req, res) => {
4087
- const secret = getStripeWebhookSecret();
4926
+ const secret = getLlamaPayWebhookSecret();
4088
4927
  if (!secret) {
4089
- return res.status(503).json(BILLING_UNAVAILABLE);
4928
+ return res.status(503).json(LLAMAPAY_UNAVAILABLE);
4090
4929
  }
4091
4930
  const raw = req.body;
4092
4931
  if (!Buffer.isBuffer(raw)) {
4093
4932
  secureLogger.error(
4094
- "Stripe webhook received a non-raw body \u2014 express.raw must mount before the JSON parser",
4933
+ "LlamaPay webhook received a non-raw body \u2014 express.raw must mount before the JSON parser",
4095
4934
  void 0,
4096
4935
  { path: req.path }
4097
4936
  );
@@ -4103,26 +4942,26 @@ var stripeWebhookHandler = asyncHandler(
4103
4942
  )
4104
4943
  );
4105
4944
  }
4106
- const verification = verifyStripeSignature({
4945
+ const verification = verifyLlamaPaySignature({
4107
4946
  payload: raw,
4108
- header: req.headers["stripe-signature"],
4947
+ header: req.headers[LLAMAPAY_SIGNATURE_HEADER],
4109
4948
  secret
4110
4949
  });
4111
4950
  if (!verification.valid) {
4112
- secureLogger.warn("Stripe webhook signature rejected", {
4951
+ secureLogger.warn("LlamaPay webhook signature rejected", {
4113
4952
  reason: verification.reason
4114
4953
  });
4115
4954
  return res.status(400).json(
4116
4955
  apiError(
4117
4956
  "authentication_error",
4118
4957
  "INVALID_WEBHOOK_SIGNATURE",
4119
- `Stripe signature verification failed (${verification.reason}).`
4958
+ `LlamaPay signature verification failed (${verification.reason}).`
4120
4959
  )
4121
4960
  );
4122
4961
  }
4123
- let event;
4962
+ let envelope;
4124
4963
  try {
4125
- event = JSON.parse(raw.toString("utf8"));
4964
+ envelope = JSON.parse(raw.toString("utf8"));
4126
4965
  } catch {
4127
4966
  return res.status(400).json(
4128
4967
  apiError(
@@ -4132,6 +4971,7 @@ var stripeWebhookHandler = asyncHandler(
4132
4971
  )
4133
4972
  );
4134
4973
  }
4974
+ const event = envelope.event ?? envelope;
4135
4975
  if (!asString(event?.id) || !asString(event?.type)) {
4136
4976
  return res.status(400).json(
4137
4977
  apiError(
@@ -4142,20 +4982,20 @@ var stripeWebhookHandler = asyncHandler(
4142
4982
  );
4143
4983
  }
4144
4984
  try {
4145
- const decision = await decideWebhookAction(event);
4985
+ const decision = decideLlamaPayAction(event);
4146
4986
  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", {
4987
+ secureLogger.warn("LlamaPay webhook ignored", {
4151
4988
  eventType: event.type,
4152
4989
  note: decision.note
4153
4990
  });
4991
+ return res.json({ received: true, handled: false });
4992
+ }
4993
+ if (decision.kind === "unresolved_user") {
4154
4994
  return res.status(500).json(
4155
4995
  apiError(
4156
4996
  "internal_error",
4157
4997
  "WEBHOOK_USER_UNRESOLVED",
4158
- "Could not resolve the Hive user for this event yet; Stripe should retry."
4998
+ "Could not resolve the Hive user for this event yet; LlamaPay should retry."
4159
4999
  )
4160
5000
  );
4161
5001
  }
@@ -4167,14 +5007,14 @@ var stripeWebhookHandler = asyncHandler(
4167
5007
  replayed: result.replayed
4168
5008
  });
4169
5009
  } catch (err) {
4170
- secureLogger.error("Stripe webhook processing failed", err, {
5010
+ secureLogger.error("LlamaPay webhook processing failed", err, {
4171
5011
  eventType: event.type
4172
5012
  });
4173
5013
  return res.status(500).json(
4174
5014
  apiError(
4175
5015
  "internal_error",
4176
5016
  "WEBHOOK_PROCESSING_FAILED",
4177
- "Webhook processing failed; Stripe should retry. Processing is idempotent by event id."
5017
+ "Webhook processing failed; LlamaPay should retry. Processing is idempotent by event id."
4178
5018
  )
4179
5019
  );
4180
5020
  }
@@ -4183,20 +5023,39 @@ var stripeWebhookHandler = asyncHandler(
4183
5023
 
4184
5024
  // src/middleware/request-id.ts
4185
5025
  import { randomUUID as randomUUID2 } from "crypto";
5026
+ function isUnstampedApiError(body) {
5027
+ if (typeof body !== "object" || body === null) return false;
5028
+ const candidate = body;
5029
+ if (candidate.ok !== false) return false;
5030
+ if (typeof candidate.error !== "object" || candidate.error === null) {
5031
+ return false;
5032
+ }
5033
+ return !("request_id" in candidate.error);
5034
+ }
4186
5035
  function requestIdMiddleware(req, res, next) {
4187
5036
  const id = req.headers["x-request-id"] || `req_${randomUUID2().replace(/-/g, "")}`;
4188
5037
  req.requestId = id;
4189
5038
  res.setHeader("x-request-id", id);
5039
+ const json = res.json.bind(res);
5040
+ res.json = (body) => {
5041
+ if (isUnstampedApiError(body)) {
5042
+ body.error.request_id = id;
5043
+ }
5044
+ return json(body);
5045
+ };
4190
5046
  next();
4191
5047
  }
4192
5048
 
4193
5049
  // src/middleware/rate-limit-headers.ts
4194
5050
  function rateLimitInfoHeaders(opts) {
4195
5051
  const windowSec = Math.round(opts.windowMs / 1e3);
4196
- return (_req, res, next) => {
5052
+ return (req, res, next) => {
4197
5053
  if (!res.getHeader("RateLimit-Limit")) {
4198
- res.setHeader("RateLimit-Limit", String(opts.limit));
4199
- res.setHeader("RateLimit-Policy", `${opts.limit};w=${windowSec}`);
5054
+ const headers = req.headers ?? {};
5055
+ const hasCredential2 = typeof headers["x-api-key"] === "string" || typeof headers.authorization === "string" && headers.authorization.startsWith("Bearer ");
5056
+ const limit = hasCredential2 && opts.credentialedLimit !== void 0 ? opts.credentialedLimit : opts.limit;
5057
+ res.setHeader("RateLimit-Limit", String(limit));
5058
+ res.setHeader("RateLimit-Policy", `${limit};w=${windowSec}`);
4200
5059
  }
4201
5060
  next();
4202
5061
  };
@@ -4600,12 +5459,13 @@ import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
4600
5459
 
4601
5460
  // src/services/anon-quota.ts
4602
5461
  import { createHash as createHash4 } from "crypto";
4603
- import { z as z4 } from "zod";
4604
- 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.";
4605
- 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.";
5462
+ import { z as z3 } from "zod";
5463
+ var ANON_QUOTA_EXHAUSTED_MESSAGE = "Anonymous free quota reached (resets 00:00 UTC). For continued access set HIVE_API_KEY (create one free at https://hiveintelligence.xyz/dashboard/keys) or sign in via OAuth.";
5464
+ 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 (create one free at https://hiveintelligence.xyz/dashboard/keys) or sign in via OAuth.";
5465
+ 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 (create one free at https://hiveintelligence.xyz/dashboard/keys) for keyed access now.";
4606
5466
  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);
5467
+ var capSchema = z3.coerce.number().int().min(1).max(1e4);
5468
+ var globalCapSchema = z3.coerce.number().int().min(1).max(1e6);
4609
5469
  function isAnonLaneEnabled() {
4610
5470
  return getEnvVar("ANON_LANE_ENABLED")?.toLowerCase() !== "false";
4611
5471
  }
@@ -4620,6 +5480,27 @@ function getAnonGlobalDailyCap() {
4620
5480
  function anonymousIpHash(ipKey) {
4621
5481
  return createHash4("sha256").update(`${ANON_IP_HASH_DOMAIN}\0${ipKey}`).digest("hex");
4622
5482
  }
5483
+ var GLOBAL_CAP_LOG_INTERVAL_MS = 6e4;
5484
+ var lastGlobalCapLoggedAt = 0;
5485
+ var globalCapRejections = 0;
5486
+ var globalCapRejectionsAtLastLog = 0;
5487
+ function noteGlobalCapTripped(limit, resetSeconds) {
5488
+ globalCapRejections += 1;
5489
+ const now = Date.now();
5490
+ if (now - lastGlobalCapLoggedAt < GLOBAL_CAP_LOG_INTERVAL_MS) return;
5491
+ lastGlobalCapLoggedAt = now;
5492
+ const rejected = globalCapRejections - globalCapRejectionsAtLastLog;
5493
+ globalCapRejectionsAtLastLog = globalCapRejections;
5494
+ secureLogger.error(
5495
+ "[anon-lane] GLOBAL daily breaker tripped \u2014 every anonymous caller is blocked until the UTC day rolls",
5496
+ void 0,
5497
+ {
5498
+ globalDailyCap: limit,
5499
+ resetSeconds,
5500
+ rejectionsSinceLastLog: rejected
5501
+ }
5502
+ );
5503
+ }
4623
5504
  async function defaultConsume(args) {
4624
5505
  const { data, error } = await getSupabaseAdmin().rpc(
4625
5506
  "consume_anon_daily_quota",
@@ -4660,11 +5541,12 @@ async function consumeAnonMaterialQuota(ipHash, overrides = {}) {
4660
5541
  resetSeconds: Math.max(1, row.reset_seconds ?? 1)
4661
5542
  };
4662
5543
  }
4663
- return {
4664
- allowed: false,
4665
- reason: row.scope === "global" ? "global_cap" : "ip_cap",
4666
- resetSeconds: Math.max(1, row.reset_seconds ?? 1)
4667
- };
5544
+ const resetSeconds = Math.max(1, row.reset_seconds ?? 1);
5545
+ if (row.scope === "global") {
5546
+ noteGlobalCapTripped(getAnonGlobalDailyCap(), resetSeconds);
5547
+ return { allowed: false, reason: "global_cap", resetSeconds };
5548
+ }
5549
+ return { allowed: false, reason: "ip_cap", resetSeconds };
4668
5550
  } catch (err) {
4669
5551
  secureLogger.warn("Anonymous quota backend unavailable; failing closed", {
4670
5552
  error: err instanceof Error ? err.message : String(err)
@@ -4693,7 +5575,7 @@ function buildServerCard() {
4693
5575
  email: "support@hiveintelligence.xyz"
4694
5576
  },
4695
5577
  homepage: "https://www.hiveintelligence.xyz",
4696
- documentation: "https://www.hiveintelligence.xyz/crypto-mcp",
5578
+ documentation: "https://docs.hiveintelligence.xyz/concepts/how-it-works",
4697
5579
  repository: {
4698
5580
  type: "git",
4699
5581
  url: "https://github.com/hive-intel/hive-sdk"
@@ -4809,7 +5691,7 @@ function buildServerCard() {
4809
5691
  freeTier: {
4810
5692
  monthlyCredits: 1e4,
4811
5693
  requestsPerMinute: 30,
4812
- description: "Demo tier \u2014 no credit card required"
5694
+ description: "Demo tier, no credit card required"
4813
5695
  }
4814
5696
  },
4815
5697
  authCompatibility,
@@ -4926,33 +5808,33 @@ function buildServerCard() {
4926
5808
  }
4927
5809
 
4928
5810
  // src/auth/oauth/router.ts
4929
- import { timingSafeEqual as timingSafeEqual4 } from "crypto";
5811
+ import { timingSafeEqual as timingSafeEqual5 } from "crypto";
4930
5812
  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";
5813
+ import rateLimit2 from "express-rate-limit";
5814
+ import RedisStore2 from "rate-limit-redis";
5815
+ import { z as z4 } from "zod";
4934
5816
  import {
4935
5817
  createOAuthMetadata,
4936
5818
  getOAuthProtectedResourceMetadataUrl,
4937
5819
  mcpAuthRouter
4938
5820
  } from "@modelcontextprotocol/sdk/server/auth/router.js";
4939
5821
  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()
5822
+ var consentDecisionSchema = z4.object({
5823
+ approved: z4.boolean(),
5824
+ scopes: z4.array(z4.enum(OAUTH_SCOPES)).max(OAUTH_SCOPES.length).optional()
4943
5825
  }).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, {
5826
+ var consentRequestIdSchema = z4.string().min(40).max(100).regex(/^hvoq_[A-Za-z0-9_-]+$/);
5827
+ var connectedAppDisconnectSchema = z4.object({
5828
+ client_id: z4.string().min(1).max(2048).refine((value) => value.trim() === value, {
4947
5829
  message: "client_id must not contain leading or trailing whitespace."
4948
5830
  })
4949
5831
  }).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({
5832
+ 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);
5833
+ var deploymentCanaryConsentSchema = z4.object({
4952
5834
  request_id: consentRequestIdSchema,
4953
5835
  client_id: deploymentCanaryClientIdSchema
4954
5836
  }).strict();
4955
- var deploymentCanaryDisconnectSchema = z5.object({ client_id: deploymentCanaryClientIdSchema }).strict();
5837
+ var deploymentCanaryDisconnectSchema = z4.object({ client_id: deploymentCanaryClientIdSchema }).strict();
4956
5838
  var OAUTH_RATE_LIMITS = {
4957
5839
  authorize: { windowMs: 15 * 60 * 1e3, max: 300 },
4958
5840
  token: { windowMs: 15 * 60 * 1e3, max: 3e3 },
@@ -4971,7 +5853,7 @@ function oauthRateLimitOptions(prefix) {
4971
5853
  const redis = getRedisClient();
4972
5854
  return {
4973
5855
  ...redis ? {
4974
- store: new RedisStore3({
5856
+ store: new RedisStore2({
4975
5857
  sendCommand: ((...args) => redis.call(
4976
5858
  args[0],
4977
5859
  ...args.slice(1)
@@ -4988,11 +5870,11 @@ function monitorSecretAuthorized(req) {
4988
5870
  if (!expected || typeof supplied !== "string") return false;
4989
5871
  const left = Buffer.from(supplied);
4990
5872
  const right = Buffer.from(expected);
4991
- return left.length === right.length && timingSafeEqual4(left, right);
5873
+ return left.length === right.length && timingSafeEqual5(left, right);
4992
5874
  }
4993
5875
  function deploymentCanaryUserId() {
4994
5876
  const userId = getEnvVar("HIVE_CANARY_USER_ID");
4995
- return userId && z5.string().uuid().safeParse(userId).success ? userId : void 0;
5877
+ return userId && z4.string().uuid().safeParse(userId).success ? userId : void 0;
4996
5878
  }
4997
5879
  async function verifyDeploymentCanaryUser(userId) {
4998
5880
  const { data, error } = await getSupabaseAdmin().auth.admin.getUserById(userId);
@@ -5010,7 +5892,7 @@ function createMcpOAuthRuntime(options = {}) {
5010
5892
  { fetcher: options.fetcher }
5011
5893
  );
5012
5894
  const serviceDocumentationUrl = new URL(
5013
- "https://www.hiveintelligence.xyz/crypto-mcp"
5895
+ "https://docs.hiveintelligence.xyz/concepts/how-it-works"
5014
5896
  );
5015
5897
  const oauthMetadata = {
5016
5898
  ...createOAuthMetadata({
@@ -5081,7 +5963,7 @@ function createMcpOAuthRuntime(options = {}) {
5081
5963
  res.json(resourceMetadata);
5082
5964
  }
5083
5965
  );
5084
- const consentRateLimit = rateLimit3({
5966
+ const consentRateLimit = rateLimit2({
5085
5967
  ...oauthRateLimitOptions("rl:oauth:consent:"),
5086
5968
  ...OAUTH_RATE_LIMITS.account,
5087
5969
  standardHeaders: true,
@@ -5107,7 +5989,7 @@ function createMcpOAuthRuntime(options = {}) {
5107
5989
  ).href;
5108
5990
  const deploymentCanaryJsonParser = express.json({ limit: "8kb" });
5109
5991
  const deploymentCanaryUserVerifier = options.deploymentCanaryUserVerifier ?? verifyDeploymentCanaryUser;
5110
- const deploymentCanaryRateLimit = rateLimit3({
5992
+ const deploymentCanaryRateLimit = rateLimit2({
5111
5993
  ...oauthRateLimitOptions("rl:oauth:deployment-canary:"),
5112
5994
  ...OAUTH_RATE_LIMITS.deploymentCanary,
5113
5995
  standardHeaders: true,
@@ -5723,7 +6605,7 @@ var port = getPort();
5723
6605
  function createHttpApp() {
5724
6606
  return app;
5725
6607
  }
5726
- app.set("trust proxy", 1);
6608
+ app.set("trust proxy", resolveTrustedProxies());
5727
6609
  app.use(requestIdMiddleware);
5728
6610
  app.use((_req, res, next) => {
5729
6611
  if (isShuttingDown) {
@@ -5739,10 +6621,12 @@ app.use((_req, res, next) => {
5739
6621
  return next();
5740
6622
  });
5741
6623
  var RATE_LIMIT_MAX = getRateLimitMax();
6624
+ var CREDENTIALED_RATE_LIMIT_MAX = getCredentialedRateLimitMax();
5742
6625
  var RATE_LIMIT_WINDOW_MS = 6e4;
5743
6626
  app.use(
5744
6627
  rateLimitInfoHeaders({
5745
6628
  limit: RATE_LIMIT_MAX,
6629
+ credentialedLimit: CREDENTIALED_RATE_LIMIT_MAX,
5746
6630
  windowMs: RATE_LIMIT_WINDOW_MS
5747
6631
  })
5748
6632
  );
@@ -5829,6 +6713,11 @@ app.post(
5829
6713
  express2.raw({ type: "application/json", limit: "1mb" }),
5830
6714
  stripeWebhookHandler
5831
6715
  );
6716
+ app.post(
6717
+ "/api/v1/billing/webhook/llamapay",
6718
+ express2.raw({ type: "application/json", limit: "1mb" }),
6719
+ llamapayWebhookHandler
6720
+ );
5832
6721
  app.use(
5833
6722
  express2.json({
5834
6723
  limit: "1mb",
@@ -5982,10 +6871,20 @@ function pingHandler(_req, res) {
5982
6871
  }
5983
6872
  app.get("/ping", pingHandler);
5984
6873
  app.get("/health", pingHandler);
5985
- var mcpIpRateLimiter = createIpRateLimiter();
5986
- var restIpRateLimiter = createIpRateLimiter();
5987
- var authIpRateLimiter = createIpRateLimiter();
5988
- app.use(["/status", "/api/health"], restIpRateLimiter);
6874
+ var mcpIpRateLimiter = [
6875
+ createIpRateLimiter({ lane: "anonymous" }),
6876
+ createIpRateLimiter({ lane: "credentialed" })
6877
+ ];
6878
+ var restIpRateLimiter = [
6879
+ createIpRateLimiter({ lane: "anonymous" }),
6880
+ createIpRateLimiter({ lane: "credentialed" })
6881
+ ];
6882
+ var authIpRateLimiter = [
6883
+ createIpRateLimiter({ lane: "anonymous" }),
6884
+ createIpRateLimiter({ lane: "credentialed" })
6885
+ ];
6886
+ var healthRateLimiter = createLocalRateLimiter();
6887
+ app.use(["/status", "/api/health"], healthRateLimiter);
5989
6888
  app.use(health_default);
5990
6889
  app.use("/auth", authIpRateLimiter);
5991
6890
  app.use("/api/v1", restIpRateLimiter);
@@ -5994,7 +6893,7 @@ app.use("/api/v1/keys", keys_default);
5994
6893
  app.use("/api/v1/usage", usage_default);
5995
6894
  app.use("/api/v1/b2b", b2b_default);
5996
6895
  app.use("/api/v1/billing", billingRouter);
5997
- var mcpMiddlewares = [mcpIpRateLimiter];
6896
+ var mcpMiddlewares = [...mcpIpRateLimiter];
5998
6897
  function withTimeout(p, ms) {
5999
6898
  return new Promise((resolve2, reject) => {
6000
6899
  const timer = setTimeout(() => {
@@ -6012,6 +6911,16 @@ function withTimeout(p, ms) {
6012
6911
  );
6013
6912
  });
6014
6913
  }
6914
+ var PRICING_URL = "https://www.hiveintelligence.xyz/pricing";
6915
+ function buildMcpQuotaExhaustedCopy(args) {
6916
+ const plan = args.planId ? `${args.planId} plan` : "current plan";
6917
+ const reset = args.cycleEnd ? `Credits refill at ${args.cycleEnd}.` : "Credits refill at the start of the next billing cycle.";
6918
+ return {
6919
+ 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.`,
6920
+ cause: `The ${plan} has no monthly credits left, so this material call cannot be debited.`,
6921
+ 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.`
6922
+ };
6923
+ }
6015
6924
  async function debitMcpCredits(args) {
6016
6925
  try {
6017
6926
  const debit = await consumeCredits({
@@ -6025,13 +6934,19 @@ async function debitMcpCredits(args) {
6025
6934
  return {
6026
6935
  ok: false,
6027
6936
  noWallet: true,
6028
- error: "This account has no credit wallet provisioned. This is an account setup issue, not a quota limit \u2014 visit https://hiveintelligence.xyz/dashboard to initialize billing or contact support."
6937
+ error: "This account has no credit wallet provisioned. This is an account setup issue, not a quota limit. Visit https://hiveintelligence.xyz/dashboard to initialize billing or contact support."
6029
6938
  };
6030
6939
  }
6031
6940
  if (debit.code === "insufficient") {
6941
+ const copy = buildMcpQuotaExhaustedCopy({
6942
+ planId: args.planId,
6943
+ cycleEnd: debit.cycleEnd
6944
+ });
6032
6945
  return {
6033
6946
  ok: false,
6034
- error: "Credit quota exceeded. Add credits or upgrade your plan to continue.",
6947
+ error: copy.message,
6948
+ cause: copy.cause,
6949
+ next_action: copy.next_action,
6035
6950
  rateLimited: true
6036
6951
  };
6037
6952
  }
@@ -6250,6 +7165,16 @@ function getMcpHiveToolName(body) {
6250
7165
  }
6251
7166
  return toolName.startsWith("hive_") ? toolName : void 0;
6252
7167
  }
7168
+ function resolveMcpClientName(req, body) {
7169
+ const fromBody = body?.params?.clientInfo?.name;
7170
+ if (typeof fromBody === "string" && fromBody.trim()) return fromBody.trim();
7171
+ const header = req.headers["x-hive-client"];
7172
+ if (typeof header === "string" && header.trim()) return header.trim();
7173
+ const userAgent = req.headers["user-agent"];
7174
+ if (typeof userAgent !== "string") return void 0;
7175
+ const product = userAgent.trim().split(/[\s\/]/, 1)[0];
7176
+ return product || void 0;
7177
+ }
6253
7178
  function mcpCredentialPresented(req) {
6254
7179
  return typeof req.headers["x-api-key"] === "string" || Boolean(req.headers.authorization?.startsWith("Bearer "));
6255
7180
  }
@@ -6288,7 +7213,11 @@ async function authorizeAnonymousMcpRequest(req) {
6288
7213
  };
6289
7214
  }
6290
7215
  if (quota.reason === "unavailable") return { kind: "quota_unavailable" };
6291
- return { kind: "quota_exhausted", resetSeconds: quota.resetSeconds };
7216
+ return {
7217
+ kind: "quota_exhausted",
7218
+ scope: quota.reason === "global_cap" ? "global" : "ip",
7219
+ resetSeconds: quota.resetSeconds
7220
+ };
6292
7221
  }
6293
7222
  function isMonitorSecretRequest(req) {
6294
7223
  const secret = getEnvVar("MONITOR_SECRET");
@@ -6296,7 +7225,7 @@ function isMonitorSecretRequest(req) {
6296
7225
  if (!secret || typeof provided !== "string") return false;
6297
7226
  const a = Buffer.from(provided);
6298
7227
  const b = Buffer.from(secret);
6299
- return a.length === b.length && timingSafeEqual5(a, b);
7228
+ return a.length === b.length && timingSafeEqual6(a, b);
6300
7229
  }
6301
7230
  function getValidatedMcpHiveToolName(scope, body) {
6302
7231
  if (body?.method !== "tools/call") return void 0;
@@ -6482,6 +7411,7 @@ function createStatelessMcpHandler(scope) {
6482
7411
  }
6483
7412
  } else if (decision.kind === "quota_exhausted") {
6484
7413
  res.setHeader("Retry-After", String(decision.resetSeconds));
7414
+ const exhaustedMessage = decision.scope === "global" ? ANON_GLOBAL_CAP_MESSAGE : ANON_QUOTA_EXHAUSTED_MESSAGE;
6485
7415
  if (mcpOAuth.status === "ready") {
6486
7416
  const requiredScopes = getRequiredMcpScopes(req.body);
6487
7417
  res.setHeader(
@@ -6493,7 +7423,7 @@ function createStatelessMcpHandler(scope) {
6493
7423
  );
6494
7424
  res.status(401).json({
6495
7425
  error: "authorization_required",
6496
- error_description: ANON_QUOTA_EXHAUSTED_MESSAGE
7426
+ error_description: exhaustedMessage
6497
7427
  });
6498
7428
  return;
6499
7429
  }
@@ -6501,9 +7431,9 @@ function createStatelessMcpHandler(scope) {
6501
7431
  jsonrpc: "2.0",
6502
7432
  error: {
6503
7433
  code: -32001,
6504
- message: ANON_QUOTA_EXHAUSTED_MESSAGE,
7434
+ message: exhaustedMessage,
6505
7435
  data: {
6506
- code: "ANON_QUOTA_EXCEEDED",
7436
+ code: decision.scope === "global" ? "ANON_GLOBAL_CAP_EXCEEDED" : "ANON_QUOTA_EXCEEDED",
6507
7437
  retryable: true,
6508
7438
  retry_after: decision.resetSeconds
6509
7439
  }
@@ -6601,6 +7531,30 @@ function createStatelessMcpHandler(scope) {
6601
7531
  return;
6602
7532
  }
6603
7533
  }
7534
+ if (keyedAuth?.userId) {
7535
+ const decision = await enforceUserRateLimit(keyedAuth.userId);
7536
+ res.setHeader("RateLimit-Limit", String(decision.limit));
7537
+ res.setHeader("RateLimit-Remaining", String(decision.remaining));
7538
+ res.setHeader("RateLimit-Reset", String(decision.resetSeconds));
7539
+ res.setHeader("RateLimit-Policy", `${decision.limit};w=60`);
7540
+ if (!decision.allowed) {
7541
+ res.setHeader("Retry-After", String(decision.resetSeconds));
7542
+ res.status(200).json({
7543
+ jsonrpc: "2.0",
7544
+ error: {
7545
+ code: -32001,
7546
+ 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.`,
7547
+ data: {
7548
+ code: "RATE_LIMITED",
7549
+ retryable: true,
7550
+ retry_after: decision.resetSeconds
7551
+ }
7552
+ },
7553
+ id: req.body?.id ?? null
7554
+ });
7555
+ return;
7556
+ }
7557
+ }
6604
7558
  try {
6605
7559
  const body = req.body;
6606
7560
  const rawHiveToolName = getMcpHiveToolName(body);
@@ -6651,11 +7605,10 @@ function createStatelessMcpHandler(scope) {
6651
7605
  subjectContext = subject.data;
6652
7606
  }
6653
7607
  sanitizeMcpAuthContext(body);
6654
- const clientInfoName = body?.params?.clientInfo?.name;
6655
7608
  const analytics = {
6656
7609
  principalRaw: keyedAuth?.userId ?? anonymousLane?.principalRaw,
6657
7610
  authLane: keyedAuth ? keyedAuth.authMethod : "anonymous",
6658
- clientName: typeof clientInfoName === "string" ? clientInfoName : void 0,
7611
+ clientName: resolveMcpClientName(req, body),
6659
7612
  isInternal: isMonitorSecretRequest(req)
6660
7613
  };
6661
7614
  const server = createHiveMcpServer(scope, {
@@ -6719,7 +7672,8 @@ function createStatelessMcpHandler(scope) {
6719
7672
  const debit = await debitMcpCredits({
6720
7673
  userId: keyedAuth.userId,
6721
7674
  keyId: keyedAuth.keyId,
6722
- requestId: req.requestId
7675
+ requestId: req.requestId,
7676
+ planId: typeof keyedAuth.keyMeta?.plan === "string" ? keyedAuth.keyMeta.plan : void 0
6723
7677
  });
6724
7678
  if (!debit.ok) {
6725
7679
  if (debit.noWallet) {
@@ -6737,7 +7691,9 @@ function createStatelessMcpHandler(scope) {
6737
7691
  type: debit.rateLimited ? "rate_limit_error" : "internal_error",
6738
7692
  code: debit.rateLimited ? "QUOTA_EXCEEDED" : "QUOTA_SERVICE_UNAVAILABLE",
6739
7693
  message: debit.error,
6740
- runtimeStatus: debit.rateLimited ? "rate_limited" : "failing"
7694
+ runtimeStatus: debit.rateLimited ? "rate_limited" : "failing",
7695
+ ...debit.cause ? { cause: debit.cause } : {},
7696
+ ...debit.next_action ? { next_action: debit.next_action } : {}
6741
7697
  };
6742
7698
  }
6743
7699
  res.setHeader(
@@ -6756,16 +7712,18 @@ function createStatelessMcpHandler(scope) {
6756
7712
  };
6757
7713
  }
6758
7714
  }
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
- }
7715
+ const auditEvent = buildStatefulWriteAuditEvent({
7716
+ isStatefulWrite: isHiveStatefulWriteTool(executedToolName),
7717
+ transport: "mcp",
7718
+ toolName: executedToolName,
7719
+ userId: keyedAuth.userId,
7720
+ subjectContext,
7721
+ keyId: keyedAuth.keyId,
7722
+ authMethod: keyedAuth.authMethod,
7723
+ oauthClientId: keyedAuth.oauthClientId,
7724
+ requestId: req.requestId
7725
+ });
7726
+ if (auditEvent) recordSubjectAuditEvent(auditEvent);
6769
7727
  return void 0;
6770
7728
  } : anonymousLane ? async (executedToolName) => {
6771
7729
  const creditCost = getMcpCreditCost(executedToolName);
@@ -6775,7 +7733,7 @@ function createStatelessMcpHandler(scope) {
6775
7733
  return {
6776
7734
  type: "authentication_error",
6777
7735
  code: "ANON_AUTH_REQUIRED",
6778
- message: "This tool requires authentication. Set HIVE_API_KEY \u2014 create one free at https://hiveintelligence.xyz/dashboard/keys \u2014 or sign in via OAuth.",
7736
+ message: "This tool requires authentication. Set HIVE_API_KEY (create one free at https://hiveintelligence.xyz/dashboard/keys) or sign in via OAuth.",
6779
7737
  runtimeStatus: "failing"
6780
7738
  };
6781
7739
  }
@@ -6868,7 +7826,7 @@ var requireMcpTransportCredential = async (req, res, next) => {
6868
7826
  };
6869
7827
  app.get(
6870
7828
  mcpPaths,
6871
- mcpIpRateLimiter,
7829
+ ...mcpIpRateLimiter,
6872
7830
  requireMcpTransportCredential,
6873
7831
  (_req, res) => {
6874
7832
  res.writeHead(405, { Allow: "POST" }).end();
@@ -6876,7 +7834,7 @@ app.get(
6876
7834
  );
6877
7835
  app.delete(
6878
7836
  mcpPaths,
6879
- mcpIpRateLimiter,
7837
+ ...mcpIpRateLimiter,
6880
7838
  requireMcpTransportCredential,
6881
7839
  (_req, res) => {
6882
7840
  res.writeHead(405, { Allow: "POST" }).end();
@@ -6884,7 +7842,7 @@ app.delete(
6884
7842
  );
6885
7843
  app.post(
6886
7844
  "/api/run-tests",
6887
- restIpRateLimiter,
7845
+ ...restIpRateLimiter,
6888
7846
  asyncHandler(async (req, res) => {
6889
7847
  const authToken = req.headers["x-monitor-secret"];
6890
7848
  const monitorSecret = getEnvVar("MONITOR_SECRET");
@@ -6892,7 +7850,7 @@ app.post(
6892
7850
  if (monitorSecret && typeof authToken === "string") {
6893
7851
  const a = Buffer.from(authToken);
6894
7852
  const b = Buffer.from(monitorSecret);
6895
- isAuthorized = a.length === b.length && timingSafeEqual5(a, b);
7853
+ isAuthorized = a.length === b.length && timingSafeEqual6(a, b);
6896
7854
  }
6897
7855
  secureLogger.authEvent("monitor-tests-access", isAuthorized, {
6898
7856
  hasToken: !!authToken,
@@ -7041,15 +7999,50 @@ function getRestHeroEndpoint(toolName) {
7041
7999
  }
7042
8000
  return restHeroEndpointsByName.get(toolName);
7043
8001
  }
8002
+ var knownRestToolNames = null;
8003
+ function isKnownRestToolName(toolName) {
8004
+ if (!knownRestToolNames) {
8005
+ knownRestToolNames = new Set(
8006
+ getAllWrappedEndpoints().map((endpoint) => endpoint.tool.name)
8007
+ );
8008
+ }
8009
+ return knownRestToolNames.has(toolName);
8010
+ }
7044
8011
  var anonymousHeroExecuteMiddleware = asyncHandler(
7045
8012
  async (req, res, next) => {
8013
+ const rawBody = req.body;
8014
+ if (rawBody && typeof rawBody === "object" && rawBody.args === void 0 && rawBody.arguments !== void 0) {
8015
+ rawBody.args = rawBody.arguments;
8016
+ }
7046
8017
  if (mcpCredentialPresented(req)) {
7047
8018
  next();
7048
8019
  return;
7049
8020
  }
7050
- const body = req.body;
8021
+ const body = rawBody;
7051
8022
  const toolName = typeof body?.tool === "string" ? body.tool : void 0;
7052
- if (!toolName || !HERO_TOOL_NAMES.has(toolName)) {
8023
+ if (!toolName) {
8024
+ res.status(400).json({
8025
+ ok: false,
8026
+ error: {
8027
+ type: "invalid_request",
8028
+ code: "TOOL_REQUIRED",
8029
+ message: `Missing required field "tool". Send {"tool":"<name>","args":{...}}. The keyless lane serves ${[...HERO_TOOL_NAMES].join(", ")}; every other tool needs an API key.`
8030
+ }
8031
+ });
8032
+ return;
8033
+ }
8034
+ if (!HERO_TOOL_NAMES.has(toolName) && !isKnownRestToolName(toolName)) {
8035
+ res.status(404).json({
8036
+ ok: false,
8037
+ error: {
8038
+ type: "invalid_request",
8039
+ code: "TOOL_NOT_FOUND",
8040
+ message: `Unknown tool "${toolName}". Browse the catalog at /api/v1/tools, or call search_tools over MCP.`
8041
+ }
8042
+ });
8043
+ return;
8044
+ }
8045
+ if (!HERO_TOOL_NAMES.has(toolName)) {
7053
8046
  next();
7054
8047
  return;
7055
8048
  }
@@ -7077,12 +8070,13 @@ var anonymousHeroExecuteMiddleware = asyncHandler(
7077
8070
  );
7078
8071
  return;
7079
8072
  }
8073
+ const globalCap = quota.reason === "global_cap";
7080
8074
  res.setHeader("Retry-After", String(quota.resetSeconds));
7081
8075
  res.status(429).json(
7082
8076
  apiError(
7083
8077
  "rate_limit_error",
7084
- "ANON_QUOTA_EXCEEDED",
7085
- ANON_QUOTA_EXHAUSTED_MESSAGE,
8078
+ globalCap ? "ANON_GLOBAL_CAP_EXCEEDED" : "ANON_QUOTA_EXCEEDED",
8079
+ globalCap ? ANON_GLOBAL_CAP_MESSAGE : ANON_QUOTA_EXHAUSTED_MESSAGE,
7086
8080
  { requestId: req.requestId }
7087
8081
  )
7088
8082
  );
@@ -7096,6 +8090,7 @@ var anonymousHeroExecuteMiddleware = asyncHandler(
7096
8090
  analytics: {
7097
8091
  principalRaw: `anon:${ipHash}`,
7098
8092
  authLane: "anonymous",
8093
+ clientName: resolveMcpClientName(req, body),
7099
8094
  isInternal: isMonitorSecretRequest(req)
7100
8095
  }
7101
8096
  });
@@ -7168,16 +8163,18 @@ app.post(
7168
8163
  };
7169
8164
  }
7170
8165
  }
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
- }
8166
+ const restAuditEvent = buildStatefulWriteAuditEvent({
8167
+ isStatefulWrite: isHiveStatefulWriteTool(endpoint.tool.name),
8168
+ transport: "rest",
8169
+ toolName: endpoint.tool.name,
8170
+ userId: authReq.userId,
8171
+ subjectContext: authReq.hiveSubjectContext,
8172
+ keyId: authReq.unkeyKeyId,
8173
+ // REST is the API-key transport; OAuth tokens are MCP-only.
8174
+ authMethod: "api_key",
8175
+ requestId: req.requestId
8176
+ });
8177
+ if (restAuditEvent) recordSubjectAuditEvent(restAuditEvent);
7181
8178
  return void 0;
7182
8179
  };
7183
8180
  const heroEndpoint = HERO_TOOL_NAMES.has(executionToolName) ? getRestHeroEndpoint(executionToolName) : void 0;
@@ -7229,13 +8226,13 @@ app.post(
7229
8226
  });
7230
8227
  })
7231
8228
  );
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)
8229
+ var cliTelemetrySchema = z5.object({
8230
+ command: z5.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9:_-]+$/),
8231
+ durationMs: z5.number().int().min(0).max(6e5),
8232
+ errorCode: z5.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9:_-]+$/).optional(),
8233
+ cliVersion: z5.string().trim().min(1).max(40),
8234
+ os: z5.string().trim().min(1).max(120),
8235
+ nodeVersion: z5.string().trim().min(1).max(40)
7239
8236
  }).strict();
7240
8237
  app.post("/api/v1/telemetry", (req, res) => {
7241
8238
  const parsed = cliTelemetrySchema.safeParse(req.body);
@@ -7324,6 +8321,10 @@ async function setupServer() {
7324
8321
  `OAuth configuration is incomplete: ${mcpOAuth.config.issues.join(" ")}`
7325
8322
  );
7326
8323
  }
8324
+ const redisReady = await waitForRedisAvailability(3e3);
8325
+ secureLogger.info("[startup] Redis readiness settled", {
8326
+ ready: redisReady
8327
+ });
7327
8328
  secureLogger.info("Setting up Hive MCP server...", {
7328
8329
  server: SERVER_NAME,
7329
8330
  version: SERVER_VERSION,
@@ -7420,21 +8421,41 @@ async function startHttpServer() {
7420
8421
  });
7421
8422
  httpServer.keepAliveTimeout = 65e3;
7422
8423
  httpServer.headersTimeout = 7e4;
8424
+ const ANALYTICS_FLUSH_TIMEOUT_MS = 2e3;
8425
+ const flushAnalytics = async () => {
8426
+ const writer = getAnalyticsWriter();
8427
+ await Promise.race([
8428
+ writer.flush(),
8429
+ new Promise((resolve2) => {
8430
+ setTimeout(resolve2, ANALYTICS_FLUSH_TIMEOUT_MS).unref();
8431
+ })
8432
+ ]);
8433
+ const stats = writer.getStats();
8434
+ secureLogger.info("Analytics queue flushed on shutdown", {
8435
+ stillQueued: stats.queued,
8436
+ droppedTotal: stats.dropped
8437
+ });
8438
+ };
8439
+ const exitAfterAnalyticsFlush = (code) => {
8440
+ getAnalyticsWriter().stop();
8441
+ void flushAnalytics().finally(() => process.exit(code));
8442
+ };
7423
8443
  const shutdown = (signal) => {
7424
8444
  if (isShuttingDown) return;
7425
8445
  isShuttingDown = true;
7426
8446
  secureLogger.info(
7427
8447
  `${signal} received \u2014 draining ${activeRequests} active requests`
7428
8448
  );
8449
+ void flushAnalytics();
7429
8450
  httpServer.close(() => {
7430
8451
  secureLogger.info("HTTP server closed", { activeRequests });
7431
- process.exit(0);
8452
+ exitAfterAnalyticsFlush(0);
7432
8453
  });
7433
8454
  const drainInterval = setInterval(() => {
7434
8455
  if (activeRequests <= 0) {
7435
8456
  clearInterval(drainInterval);
7436
8457
  secureLogger.info("All requests drained, closing");
7437
- httpServer.close(() => process.exit(0));
8458
+ httpServer.close(() => exitAfterAnalyticsFlush(0));
7438
8459
  }
7439
8460
  }, 500);
7440
8461
  drainInterval.unref();
@@ -7443,7 +8464,7 @@ async function startHttpServer() {
7443
8464
  secureLogger.warn("Force shutdown after 10s timeout", {
7444
8465
  activeRequests
7445
8466
  });
7446
- process.exit(1);
8467
+ exitAfterAnalyticsFlush(1);
7447
8468
  }, 1e4);
7448
8469
  forceTimer.unref();
7449
8470
  };
@@ -7466,9 +8487,11 @@ if (isMainModule()) {
7466
8487
  }
7467
8488
  var server_default = app;
7468
8489
  export {
8490
+ buildMcpQuotaExhaustedCopy,
7469
8491
  classifyAnonymousMcpRequest,
7470
8492
  createHttpApp,
7471
8493
  server_default as default,
7472
8494
  getRequiredMcpScopes,
8495
+ resolveMcpClientName,
7473
8496
  startHttpServer
7474
8497
  };