hippo-memory 1.38.0 → 1.38.2

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 (79) hide show
  1. package/dist/capture.js +1 -1
  2. package/dist/capture.js.map +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/compare.d.ts +26 -13
  6. package/dist/compare.d.ts.map +1 -1
  7. package/dist/compare.js +50 -20
  8. package/dist/compare.js.map +1 -1
  9. package/dist/consolidate.js +5 -4
  10. package/dist/consolidate.js.map +1 -1
  11. package/dist/db.d.ts.map +1 -1
  12. package/dist/db.js +61 -0
  13. package/dist/db.js.map +1 -1
  14. package/dist/dedupe.d.ts +9 -7
  15. package/dist/dedupe.d.ts.map +1 -1
  16. package/dist/dedupe.js +12 -8
  17. package/dist/dedupe.js.map +1 -1
  18. package/dist/extensions/openclaw-plugin/index.js +585 -0
  19. package/dist/extensions/openclaw-plugin/index.js.map +1 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/mcp/server.d.ts +3 -0
  25. package/dist/mcp/server.d.ts.map +1 -1
  26. package/dist/mcp/server.js +7 -16
  27. package/dist/mcp/server.js.map +1 -1
  28. package/dist/memory-value.js +1 -1
  29. package/dist/memory-value.js.map +1 -1
  30. package/dist/project-identity.d.ts +8 -0
  31. package/dist/project-identity.d.ts.map +1 -1
  32. package/dist/project-identity.js +34 -18
  33. package/dist/project-identity.js.map +1 -1
  34. package/dist/server.d.ts +28 -4
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +62 -19
  37. package/dist/server.js.map +1 -1
  38. package/dist/sleep-redact.d.ts +7 -6
  39. package/dist/sleep-redact.d.ts.map +1 -1
  40. package/dist/sleep-redact.js +7 -6
  41. package/dist/sleep-redact.js.map +1 -1
  42. package/dist/src/capture.js +1 -1
  43. package/dist/src/capture.js.map +1 -1
  44. package/dist/src/cli.js +1 -1
  45. package/dist/src/cli.js.map +1 -1
  46. package/dist/src/compare.js +50 -20
  47. package/dist/src/compare.js.map +1 -1
  48. package/dist/src/consolidate.js +5 -4
  49. package/dist/src/consolidate.js.map +1 -1
  50. package/dist/src/db.js +61 -0
  51. package/dist/src/db.js.map +1 -1
  52. package/dist/src/dedupe.js +12 -8
  53. package/dist/src/dedupe.js.map +1 -1
  54. package/dist/src/index.js +2 -0
  55. package/dist/src/index.js.map +1 -1
  56. package/dist/src/mcp/server.js +7 -16
  57. package/dist/src/mcp/server.js.map +1 -1
  58. package/dist/src/memory-value.js +1 -1
  59. package/dist/src/memory-value.js.map +1 -1
  60. package/dist/src/project-identity.js +34 -18
  61. package/dist/src/project-identity.js.map +1 -1
  62. package/dist/src/server.js +62 -19
  63. package/dist/src/server.js.map +1 -1
  64. package/dist/src/sleep-redact.js +7 -6
  65. package/dist/src/sleep-redact.js.map +1 -1
  66. package/dist/src/store.js +4 -3
  67. package/dist/src/store.js.map +1 -1
  68. package/dist/src/version.js +1 -1
  69. package/dist/store.d.ts +3 -1
  70. package/dist/store.d.ts.map +1 -1
  71. package/dist/store.js +4 -3
  72. package/dist/store.js.map +1 -1
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/extensions/openclaw-plugin/index.ts +7 -7
  76. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  77. package/extensions/openclaw-plugin/package.json +2 -1
  78. package/openclaw.plugin.json +1 -1
  79. package/package.json +3 -3
package/dist/server.js CHANGED
@@ -391,6 +391,32 @@ function buildMcpClientKey(req) {
391
391
  const addr = req.socket.remoteAddress ?? 'unknown';
392
392
  return `http:${tokenHash}:${addr}`;
393
393
  }
394
+ /**
395
+ * Rate-limit key for a request. Defaults to the socket's remote address.
396
+ *
397
+ * Behind a TLS-terminating proxy (Fly, most PaaS ingress) every socket
398
+ * carries the proxy's address, so per-IP buckets collapse into one global
399
+ * bucket that unauthenticated traffic can drain before auth runs. Set
400
+ * HIPPO_CLIENT_IP_HEADER to the header the proxy stamps with the real
401
+ * client address (fly-client-ip on Fly, which the edge always overwrites)
402
+ * to key buckets per client instead.
403
+ *
404
+ * Only set this when a trusted proxy fronts EVERY request: a directly
405
+ * reachable server honoring the header would let clients mint a fresh
406
+ * bucket per request and bypass the limiter entirely.
407
+ */
408
+ export function clientIpForRateLimit(req) {
409
+ const header = process.env.HIPPO_CLIENT_IP_HEADER?.toLowerCase();
410
+ if (header) {
411
+ const raw = req.headers[header];
412
+ const first = Array.isArray(raw) ? raw[0] : raw;
413
+ // Take the first entry of a comma-joined list (proxy chains append).
414
+ const ip = first?.split(',')[0]?.trim();
415
+ if (ip)
416
+ return ip;
417
+ }
418
+ return req.socket.remoteAddress ?? 'unknown';
419
+ }
394
420
  /**
395
421
  * Build a per-request Context from the Authorization header and remote
396
422
  * address. Throws HttpError(401) for invalid / missing credentials. Opens
@@ -526,25 +552,34 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
526
552
  rejectEncodedSlash(req.url ?? '/');
527
553
  const { method, path, query } = parseRequest(req);
528
554
  if (method === 'GET' && path === '/health') {
529
- sendJson(res, 200, {
530
- ok: true,
531
- version: VERSION,
532
- started_at: startedAt,
533
- pid: process.pid,
534
- });
555
+ // Loopback callers (detectServer's stale-pidfile probe reads version and
556
+ // pid) get the full body. Non-loopback callers get liveness only: the
557
+ // version string would fingerprint the build for the public internet and
558
+ // the pid is noise. Platform health checks only need the 200.
559
+ if (isLoopback(req.socket.remoteAddress)) {
560
+ sendJson(res, 200, {
561
+ ok: true,
562
+ version: VERSION,
563
+ started_at: startedAt,
564
+ pid: process.pid,
565
+ });
566
+ }
567
+ else {
568
+ sendJson(res, 200, { ok: true });
569
+ }
535
570
  return;
536
571
  }
537
572
  // E3: per-IP rate limit on /v1/* to bound api-key-id enumeration. /health
538
573
  // (a liveness probe) and non-/v1 paths are never throttled. A 429 thrown
539
574
  // here lands in the createServer catch like any other HttpError.
540
575
  //
541
- // Keyed on the socket's remote address. serve() binds loopback-only today,
542
- // so in the default deployment this is effectively one global /v1 bucket,
543
- // which still bounds enumeration. True per-client keying (and trusting an
544
- // X-Forwarded-For only from a known proxy) belongs with the non-loopback
545
- // serving that A5 v2 unlocks.
576
+ // Keyed on the socket's remote address by default. Behind a TLS-terminating
577
+ // proxy every socket carries the proxy's address, collapsing the per-IP
578
+ // buckets into one global bucket that pre-auth traffic can drain; set
579
+ // HIPPO_CLIENT_IP_HEADER there so each real client gets its own bucket
580
+ // (see clientIpForRateLimit).
546
581
  if (limiter && path.startsWith('/v1/')) {
547
- const ip = req.socket.remoteAddress ?? 'unknown';
582
+ const ip = clientIpForRateLimit(req);
548
583
  if (!limiter.check(ip)) {
549
584
  throw new HttpError(429, 'rate limit exceeded');
550
585
  }
@@ -3051,10 +3086,18 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
3051
3086
  /**
3052
3087
  * Boot the HTTP daemon on host:port and write the pidfile under hippoRoot.
3053
3088
  *
3054
- * Refuses non-loopback hosts at boot (Footgun #3 from the A1 plan): without
3055
- * the A5 v2 auth middleware we have no way to gate remote requests, so we
3056
- * fail fast rather than expose the DB to the network. Task 9 will lift this
3057
- * restriction once Bearer-token validation lands.
3089
+ * Refuses non-loopback hosts at boot (Footgun #3 from the A1 plan) unless
3090
+ * HIPPO_REQUIRE_AUTH=1 is set. The A5 v2 auth middleware (buildContextWithAuth /
3091
+ * requireAuth) has shipped and every route checks it except GET /health
3092
+ * (public by design for platform health checks) and the two connector
3093
+ * webhooks in PUBLIC_ROUTES, which are HMAC-gated by their own signing
3094
+ * secrets and 404 when those secrets are unset. But the loopback
3095
+ * no-auth fallback inside buildContextWithAuth still admits unauthenticated
3096
+ * requests from a loopback remote address, so binding to a non-loopback host
3097
+ * is only safe once that fallback is disabled with HIPPO_REQUIRE_AUTH=1,
3098
+ * which forces every request (loopback or not) through Bearer-token
3099
+ * validation. Without that env var set, a non-loopback bind would expose the
3100
+ * DB to the network with no auth, so we fail fast instead.
3058
3101
  *
3059
3102
  * Use port: 0 in tests to bind to an ephemeral port and read the actual
3060
3103
  * port back via server.address() after listen.
@@ -3062,10 +3105,10 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
3062
3105
  export async function serve(opts) {
3063
3106
  const host = opts.host ?? '127.0.0.1';
3064
3107
  const requestedPort = opts.port ?? Number(process.env.HIPPO_PORT ?? 6789);
3065
- if (!LOOPBACK_HOSTS.has(host)) {
3108
+ if (!LOOPBACK_HOSTS.has(host) && process.env.HIPPO_REQUIRE_AUTH !== '1') {
3066
3109
  throw new Error(`Refusing to bind hippo serve to non-loopback host '${host}' without auth. ` +
3067
- `Remote-host serving requires the A5 v2 auth middleware (Task 9 of the A1 plan). ` +
3068
- `Bind to 127.0.0.1 / ::1 / localhost, or wait for auth support.`);
3110
+ `Set HIPPO_REQUIRE_AUTH=1 to bind non-loopback; every request then requires ` +
3111
+ `a valid API key. Bind to 127.0.0.1 / ::1 / localhost otherwise.`);
3069
3112
  }
3070
3113
  // H3: refuse to start if a live hippo server already serves this hippoRoot.
3071
3114
  // detectServer probes the recorded /health — a stale pidfile is unlinked and