hippo-memory 1.59.0 → 1.61.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.
- package/README.md +1 -1
- package/dist/ablation.js +9 -27
- package/dist/agent-memories/apply.js +4 -1
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/codex.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/agent-memories/legacy.js +1 -1
- package/dist/agent-memories/source.js +1 -1
- package/dist/agent-memories/sync.js +8 -3
- package/dist/ambient-store.d.ts +14 -0
- package/dist/ambient-store.js +90 -0
- package/dist/ambient.d.ts +23 -0
- package/dist/ambient.js +72 -50
- package/dist/api/assemble.d.ts +93 -0
- package/dist/api/assemble.js +152 -0
- package/dist/api/audit.d.ts +17 -0
- package/dist/api/audit.js +23 -0
- package/dist/api/auth.d.ts +79 -0
- package/dist/api/auth.js +178 -0
- package/dist/api/context-types.d.ts +105 -0
- package/dist/api/context-types.js +3 -0
- package/dist/api/context.d.ts +31 -0
- package/dist/api/context.js +705 -0
- package/dist/api/dormant.d.ts +30 -0
- package/dist/api/dormant.js +140 -0
- package/dist/api/drill-down.d.ts +84 -0
- package/dist/api/drill-down.js +123 -0
- package/dist/api/forget.d.ts +57 -0
- package/dist/api/forget.js +87 -0
- package/dist/api/goals.d.ts +18 -0
- package/dist/api/goals.js +33 -0
- package/dist/api/learn.d.ts +31 -0
- package/dist/api/learn.js +88 -0
- package/dist/api/outcome.d.ts +61 -0
- package/dist/api/outcome.js +66 -0
- package/dist/api/promote.d.ts +54 -0
- package/dist/api/promote.js +203 -0
- package/dist/api/quarantine.d.ts +24 -0
- package/dist/api/quarantine.js +121 -0
- package/dist/api/recall-types.d.ts +390 -0
- package/dist/api/recall-types.js +3 -0
- package/dist/api/recall.d.ts +36 -0
- package/dist/api/recall.js +634 -0
- package/dist/api/remember.d.ts +35 -0
- package/dist/api/remember.js +43 -0
- package/dist/api/sleep.d.ts +136 -0
- package/dist/api/sleep.js +271 -0
- package/dist/api/tokens.d.ts +26 -0
- package/dist/api/tokens.js +60 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/api/types.js +38 -0
- package/dist/api.d.ts +20 -1260
- package/dist/api.js +25 -2711
- package/dist/audit.d.ts +3 -0
- package/dist/audit.js +7 -3
- package/dist/auth.d.ts +45 -4
- package/dist/auth.js +125 -48
- package/dist/autolearn.js +7 -4
- package/dist/capture/command.d.ts +33 -0
- package/dist/capture/command.js +264 -0
- package/dist/capture/compact.d.ts +44 -0
- package/dist/capture/compact.js +354 -0
- package/dist/capture/extract.d.ts +21 -0
- package/dist/capture/extract.js +464 -0
- package/dist/capture/transcript.d.ts +40 -0
- package/dist/capture/transcript.js +193 -0
- package/dist/capture-error.js +2 -1
- package/dist/churn-git.js +4 -2
- package/dist/cli/audit.d.ts +3 -0
- package/dist/cli/audit.js +159 -0
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.js +171 -0
- package/dist/cli/briefs.d.ts +4 -0
- package/dist/cli/briefs.js +435 -0
- package/dist/cli/card.d.ts +3 -0
- package/dist/cli/card.js +333 -0
- package/dist/cli/context.d.ts +15 -0
- package/dist/cli/context.js +366 -0
- package/dist/cli/continuity.d.ts +6 -0
- package/dist/cli/continuity.js +445 -0
- package/dist/cli/curate.d.ts +19 -0
- package/dist/cli/curate.js +555 -0
- package/dist/cli/dag.d.ts +5 -0
- package/dist/cli/dag.js +177 -0
- package/dist/cli/decisions.d.ts +4 -0
- package/dist/cli/decisions.js +528 -0
- package/dist/cli/eval.d.ts +5 -0
- package/dist/cli/eval.js +213 -0
- package/dist/cli/explain.d.ts +4 -0
- package/dist/cli/explain.js +150 -0
- package/dist/cli/goals.d.ts +2 -0
- package/dist/cli/goals.js +196 -0
- package/dist/cli/hook-blocks.d.ts +18 -0
- package/dist/cli/hook-blocks.js +233 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +305 -0
- package/dist/cli/maintenance.d.ts +4 -0
- package/dist/cli/maintenance.js +179 -0
- package/dist/cli/playbooks.d.ts +4 -0
- package/dist/cli/playbooks.js +556 -0
- package/dist/cli/projects.js +1 -1
- package/dist/cli/recall.d.ts +7 -0
- package/dist/cli/recall.js +597 -0
- package/dist/cli/remember.d.ts +5 -0
- package/dist/cli/remember.js +442 -0
- package/dist/cli/serve.d.ts +5 -0
- package/dist/cli/serve.js +40 -0
- package/dist/cli/session-hooks.d.ts +29 -0
- package/dist/cli/session-hooks.js +637 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.js +376 -0
- package/dist/cli/shared.d.ts +15 -19
- package/dist/cli/shared.js +50 -341
- package/dist/cli/slack.d.ts +2 -0
- package/dist/cli/slack.js +171 -0
- package/dist/cli/status.d.ts +18 -0
- package/dist/cli/status.js +400 -0
- package/dist/cli/transfer.d.ts +10 -0
- package/dist/cli/transfer.js +438 -0
- package/dist/cli/usage.d.ts +85 -0
- package/dist/cli/usage.js +741 -0
- package/dist/cli.d.ts +319 -13
- package/dist/cli.js +436 -8580
- package/dist/client.js +15 -8
- package/dist/compaction-record.js +12 -9
- package/dist/config.js +12 -11
- package/dist/connectors/github/backfill.js +94 -87
- package/dist/connectors/github/cli-impl.js +5 -3
- package/dist/connectors/github/dlq.js +67 -54
- package/dist/connectors/github/ingest.js +34 -35
- package/dist/connectors/github/tenant-routing.js +3 -2
- package/dist/connectors/github/webhook.js +135 -216
- package/dist/connectors/slack/dlq.js +49 -61
- package/dist/connectors/slack/ingest.js +55 -48
- package/dist/connectors/slack/tenant-routing.js +4 -3
- package/dist/connectors/slack/webhook.js +72 -74
- package/dist/consolidate/conflicts.d.ts +10 -0
- package/dist/consolidate/conflicts.js +178 -0
- package/dist/consolidate/decay.d.ts +12 -0
- package/dist/consolidate/decay.js +145 -0
- package/dist/consolidate/llm-passes.d.ts +3 -0
- package/dist/consolidate/llm-passes.js +141 -0
- package/dist/consolidate/merge.d.ts +7 -0
- package/dist/consolidate/merge.js +251 -0
- package/dist/consolidate/physics-pass.d.ts +3 -0
- package/dist/consolidate/physics-pass.js +60 -0
- package/dist/consolidate/run.d.ts +69 -0
- package/dist/consolidate/run.js +76 -0
- package/dist/consolidate/sleep.d.ts +18 -0
- package/dist/consolidate/sleep.js +209 -0
- package/dist/consolidate/traces.d.ts +4 -0
- package/dist/consolidate/traces.js +178 -0
- package/dist/context-auto.js +7 -11
- package/dist/context-render.d.ts +1 -1
- package/dist/context-render.js +1 -1
- package/dist/customer-notes.d.ts +3 -0
- package/dist/customer-notes.js +6 -4
- package/dist/dag.js +13 -12
- package/dist/dashboard-actions.d.ts +20 -0
- package/dist/dashboard-actions.js +88 -0
- package/dist/dashboard-params.d.ts +45 -0
- package/dist/dashboard-params.js +127 -0
- package/dist/dashboard-queries.d.ts +15 -0
- package/dist/dashboard-queries.js +355 -0
- package/dist/dashboard-snapshot.d.ts +138 -0
- package/dist/dashboard-snapshot.js +308 -0
- package/dist/dashboard-types.d.ts +163 -0
- package/dist/dashboard-types.js +3 -0
- package/dist/dashboard.d.ts +6 -7
- package/dist/dashboard.js +231 -203
- package/dist/db/busy.d.ts +5 -0
- package/dist/db/busy.js +24 -0
- package/dist/db/continuity.d.ts +5 -0
- package/dist/db/continuity.js +145 -0
- package/dist/db/meta.d.ts +8 -0
- package/dist/db/meta.js +35 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +138 -0
- package/dist/db/migrations/index.d.ts +5 -0
- package/dist/db/migrations/index.js +109 -0
- package/dist/db/migrations/types.d.ts +14 -0
- package/dist/db/migrations/types.js +2 -0
- package/dist/db/migrations/v01.d.ts +3 -0
- package/dist/db/migrations/v01.js +38 -0
- package/dist/db/migrations/v02.d.ts +3 -0
- package/dist/db/migrations/v02.js +21 -0
- package/dist/db/migrations/v03.d.ts +3 -0
- package/dist/db/migrations/v03.js +22 -0
- package/dist/db/migrations/v04.d.ts +3 -0
- package/dist/db/migrations/v04.js +28 -0
- package/dist/db/migrations/v05.d.ts +3 -0
- package/dist/db/migrations/v05.js +21 -0
- package/dist/db/migrations/v06.d.ts +3 -0
- package/dist/db/migrations/v06.js +25 -0
- package/dist/db/migrations/v07.d.ts +3 -0
- package/dist/db/migrations/v07.js +13 -0
- package/dist/db/migrations/v08.d.ts +3 -0
- package/dist/db/migrations/v08.js +8 -0
- package/dist/db/migrations/v09.d.ts +3 -0
- package/dist/db/migrations/v09.js +13 -0
- package/dist/db/migrations/v10.d.ts +3 -0
- package/dist/db/migrations/v10.js +17 -0
- package/dist/db/migrations/v11.d.ts +3 -0
- package/dist/db/migrations/v11.js +15 -0
- package/dist/db/migrations/v12.d.ts +3 -0
- package/dist/db/migrations/v12.js +11 -0
- package/dist/db/migrations/v13.d.ts +3 -0
- package/dist/db/migrations/v13.js +15 -0
- package/dist/db/migrations/v14.d.ts +3 -0
- package/dist/db/migrations/v14.js +66 -0
- package/dist/db/migrations/v15.d.ts +3 -0
- package/dist/db/migrations/v15.js +42 -0
- package/dist/db/migrations/v16.d.ts +3 -0
- package/dist/db/migrations/v16.js +61 -0
- package/dist/db/migrations/v17.d.ts +3 -0
- package/dist/db/migrations/v17.js +46 -0
- package/dist/db/migrations/v18.d.ts +3 -0
- package/dist/db/migrations/v18.js +60 -0
- package/dist/db/migrations/v19.d.ts +3 -0
- package/dist/db/migrations/v19.js +28 -0
- package/dist/db/migrations/v20.d.ts +3 -0
- package/dist/db/migrations/v20.js +42 -0
- package/dist/db/migrations/v21.d.ts +3 -0
- package/dist/db/migrations/v21.js +16 -0
- package/dist/db/migrations/v22.d.ts +3 -0
- package/dist/db/migrations/v22.js +82 -0
- package/dist/db/migrations/v23.d.ts +3 -0
- package/dist/db/migrations/v23.js +48 -0
- package/dist/db/migrations/v24.d.ts +3 -0
- package/dist/db/migrations/v24.js +72 -0
- package/dist/db/migrations/v25.d.ts +3 -0
- package/dist/db/migrations/v25.js +46 -0
- package/dist/db/migrations/v26.d.ts +3 -0
- package/dist/db/migrations/v26.js +23 -0
- package/dist/db/migrations/v27.d.ts +3 -0
- package/dist/db/migrations/v27.js +57 -0
- package/dist/db/migrations/v28.d.ts +3 -0
- package/dist/db/migrations/v28.js +38 -0
- package/dist/db/migrations/v29.d.ts +3 -0
- package/dist/db/migrations/v29.js +78 -0
- package/dist/db/migrations/v30.d.ts +3 -0
- package/dist/db/migrations/v30.js +92 -0
- package/dist/db/migrations/v31.d.ts +3 -0
- package/dist/db/migrations/v31.js +74 -0
- package/dist/db/migrations/v32.d.ts +3 -0
- package/dist/db/migrations/v32.js +102 -0
- package/dist/db/migrations/v33.d.ts +3 -0
- package/dist/db/migrations/v33.js +104 -0
- package/dist/db/migrations/v34.d.ts +3 -0
- package/dist/db/migrations/v34.js +93 -0
- package/dist/db/migrations/v35.d.ts +3 -0
- package/dist/db/migrations/v35.js +98 -0
- package/dist/db/migrations/v36.d.ts +3 -0
- package/dist/db/migrations/v36.js +98 -0
- package/dist/db/migrations/v37.d.ts +3 -0
- package/dist/db/migrations/v37.js +219 -0
- package/dist/db/migrations/v38.d.ts +3 -0
- package/dist/db/migrations/v38.js +277 -0
- package/dist/db/migrations/v39.d.ts +3 -0
- package/dist/db/migrations/v39.js +59 -0
- package/dist/db/migrations/v40.d.ts +3 -0
- package/dist/db/migrations/v40.js +74 -0
- package/dist/db/migrations/v41.d.ts +3 -0
- package/dist/db/migrations/v41.js +43 -0
- package/dist/db/migrations/v42.d.ts +3 -0
- package/dist/db/migrations/v42.js +41 -0
- package/dist/db/migrations/v43.d.ts +3 -0
- package/dist/db/migrations/v43.js +67 -0
- package/dist/db/migrations/v44.d.ts +3 -0
- package/dist/db/migrations/v44.js +28 -0
- package/dist/db/migrations/v45.d.ts +3 -0
- package/dist/db/migrations/v45.js +30 -0
- package/dist/db/migrations/v46.d.ts +3 -0
- package/dist/db/migrations/v46.js +25 -0
- package/dist/db/migrations/v47.d.ts +3 -0
- package/dist/db/migrations/v47.js +17 -0
- package/dist/db/migrations/v48.d.ts +3 -0
- package/dist/db/migrations/v48.js +10 -0
- package/dist/db/migrations/v49.d.ts +3 -0
- package/dist/db/migrations/v49.js +31 -0
- package/dist/db/migrations/v50.d.ts +3 -0
- package/dist/db/migrations/v50.js +67 -0
- package/dist/db/migrations/v51.d.ts +3 -0
- package/dist/db/migrations/v51.js +14 -0
- package/dist/db/migrations/v52.d.ts +3 -0
- package/dist/db/migrations/v52.js +7 -0
- package/dist/db/open.d.ts +23 -0
- package/dist/db/open.js +146 -0
- package/dist/db/sqlite.d.ts +21 -0
- package/dist/db/sqlite.js +8 -0
- package/dist/db/tables.d.ts +7 -0
- package/dist/db/tables.js +38 -0
- package/dist/db.d.ts +6 -46
- package/dist/db.js +5 -3036
- package/dist/decisions.d.ts +4 -1
- package/dist/decisions.js +9 -7
- package/dist/dedupe.js +3 -2
- package/dist/delivery-recorder.js +5 -1
- package/dist/doctor.js +4 -3
- package/dist/dormant.js +1 -0
- package/dist/embedding-provider.d.ts +1 -1
- package/dist/embedding-provider.js +7 -5
- package/dist/embeddings.d.ts +9 -52
- package/dist/embeddings.js +47 -298
- package/dist/env.d.ts +75 -0
- package/dist/env.js +119 -0
- package/dist/eval-suite.js +1 -1
- package/dist/eval.js +2 -2
- package/dist/extract.js +5 -4
- package/dist/gated-write.js +3 -1
- package/dist/goals.d.ts +3 -1
- package/dist/goals.js +18 -0
- package/dist/graph/read.d.ts +73 -0
- package/dist/graph/read.js +325 -0
- package/dist/graph/rows.d.ts +45 -0
- package/dist/graph/rows.js +51 -0
- package/dist/graph/types.d.ts +83 -0
- package/dist/graph/types.js +11 -0
- package/dist/graph/write.d.ts +93 -0
- package/dist/{graph.js → graph/write.js} +8 -391
- package/dist/graph-extract.js +2 -1
- package/dist/graph-recall.d.ts +1 -1
- package/dist/graph-recall.js +2 -2
- package/dist/graph-stream.js +1 -1
- package/dist/graph-view.d.ts +1 -1
- package/dist/graph-view.js +1 -1
- package/dist/half-life-migration.d.ts +1 -1
- package/dist/half-life-migration.js +2 -1
- package/dist/handoff.js +3 -0
- package/dist/hooks/codex-session.d.ts +8 -0
- package/dist/hooks/codex-session.js +76 -0
- package/dist/hooks/codex-wrapper.d.ts +55 -0
- package/dist/hooks/codex-wrapper.js +288 -0
- package/dist/hooks/json-hooks.d.ts +63 -0
- package/dist/hooks/json-hooks.js +356 -0
- package/dist/hooks/opencode.d.ts +50 -0
- package/dist/hooks/opencode.js +202 -0
- package/dist/hooks/shared.d.ts +54 -0
- package/dist/hooks/shared.js +77 -0
- package/dist/http-retry.d.ts +2 -0
- package/dist/http-retry.js +4 -3
- package/dist/http-util.d.ts +3 -0
- package/dist/http-util.js +10 -0
- package/dist/{importers.d.ts → importers/core.d.ts} +13 -17
- package/dist/importers/core.js +141 -0
- package/dist/importers/markdown-parse.d.ts +41 -0
- package/dist/importers/markdown-parse.js +132 -0
- package/dist/importers/markdown.d.ts +3 -0
- package/dist/importers/markdown.js +92 -0
- package/dist/importers/sources.d.ts +7 -0
- package/dist/importers/sources.js +229 -0
- package/dist/importers/vault.d.ts +11 -0
- package/dist/importers/vault.js +352 -0
- package/dist/incidents.d.ts +3 -0
- package/dist/incidents.js +8 -5
- package/dist/index.d.ts +25 -6
- package/dist/index.js +23 -6
- package/dist/invalidation.js +2 -1
- package/dist/judgment.js +7 -3
- package/dist/keyset.d.ts +13 -0
- package/dist/keyset.js +8 -0
- package/dist/local-embedding.d.ts +13 -0
- package/dist/local-embedding.js +165 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +19 -1
- package/dist/mcp/admin-tools.d.ts +8 -0
- package/dist/mcp/admin-tools.js +116 -0
- package/dist/mcp/format.d.ts +27 -0
- package/dist/mcp/format.js +135 -0
- package/dist/mcp/memory-tools.d.ts +5 -0
- package/dist/mcp/memory-tools.js +83 -0
- package/dist/mcp/protocol.d.ts +83 -0
- package/dist/mcp/protocol.js +55 -0
- package/dist/mcp/recall-tools.d.ts +6 -0
- package/dist/mcp/recall-tools.js +320 -0
- package/dist/mcp/request.d.ts +10 -0
- package/dist/mcp/request.js +163 -0
- package/dist/mcp/server.d.ts +4 -70
- package/dist/mcp/server.js +8 -1172
- package/dist/mcp/session-state.d.ts +11 -0
- package/dist/mcp/session-state.js +28 -0
- package/dist/mcp/stdio.d.ts +8 -0
- package/dist/mcp/stdio.js +79 -0
- package/dist/mcp/tools.d.ts +11 -0
- package/dist/mcp/tools.js +247 -0
- package/dist/memory.d.ts +8 -3
- package/dist/memory.js +27 -1
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +2 -1
- package/dist/owner-validation.js +2 -1
- package/dist/physics-state.js +10 -7
- package/dist/policies.d.ts +3 -0
- package/dist/policies.js +8 -6
- package/dist/postinstall.js +3 -2
- package/dist/predictions/planning-fallacy.d.ts +100 -0
- package/dist/predictions/planning-fallacy.js +190 -0
- package/dist/{predictions.d.ts → predictions/store.d.ts} +7 -102
- package/dist/predictions/store.js +434 -0
- package/dist/processes.d.ts +3 -0
- package/dist/processes.js +8 -5
- package/dist/project-briefs.d.ts +3 -0
- package/dist/project-briefs.js +7 -5
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +12 -6
- package/dist/project-merge.js +3 -1
- package/dist/quarantine.d.ts +2 -1
- package/dist/quarantine.js +8 -5
- package/dist/raw-archive-mirror-cleanup.js +2 -1
- package/dist/raw-archive.js +1 -1
- package/dist/recall-history.js +3 -2
- package/dist/recall-pipeline.d.ts +2 -2
- package/dist/recall-pipeline.js +16 -8
- package/dist/recall-scope.js +1 -1
- package/dist/recall-trace.d.ts +3 -3
- package/dist/recall-trace.js +10 -14
- package/dist/refine-llm.js +20 -11
- package/dist/reject-flow.js +6 -1
- package/dist/rerankers/clef.d.ts +2 -2
- package/dist/rerankers/clef.js +72 -30
- package/dist/rerankers/cross-encoder.js +6 -4
- package/dist/rerankers/jev.d.ts +4 -2
- package/dist/rerankers/jev.js +24 -19
- package/dist/rerankers/llm.d.ts +4 -2
- package/dist/rerankers/llm.js +61 -41
- package/dist/rerankers/types.d.ts +1 -1
- package/dist/salience.js +1 -1
- package/dist/same-text.d.ts +2 -0
- package/dist/same-text.js +4 -0
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +26 -3
- package/dist/scope.js +4 -3
- package/dist/search/as-of.d.ts +10 -0
- package/dist/search/as-of.js +22 -0
- package/dist/search/bm25-search.d.ts +14 -0
- package/dist/search/bm25-search.js +43 -0
- package/dist/search/bm25.d.ts +15 -0
- package/dist/search/bm25.js +54 -0
- package/dist/search/boosts.d.ts +54 -0
- package/dist/search/boosts.js +94 -0
- package/dist/search/breakdown.d.ts +7 -0
- package/dist/search/breakdown.js +20 -0
- package/dist/search/explain.d.ts +25 -0
- package/dist/search/explain.js +31 -0
- package/dist/search/finalize.d.ts +8 -0
- package/dist/search/finalize.js +52 -0
- package/dist/search/fusion.d.ts +27 -0
- package/dist/search/fusion.js +42 -0
- package/dist/search/hybrid-score.d.ts +20 -0
- package/dist/search/hybrid-score.js +73 -0
- package/dist/search/hybrid.d.ts +46 -0
- package/dist/search/hybrid.js +64 -0
- package/dist/search/physics-search.d.ts +29 -0
- package/dist/search/physics-search.js +162 -0
- package/dist/search/rerank.d.ts +10 -0
- package/dist/search/rerank.js +72 -0
- package/dist/search/temporal.d.ts +15 -0
- package/dist/search/temporal.js +45 -0
- package/dist/search/types.d.ts +91 -0
- package/dist/search/types.js +2 -0
- package/dist/search/vector.d.ts +30 -0
- package/dist/search/vector.js +71 -0
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +3 -1
- package/dist/server/auth.d.ts +52 -0
- package/dist/server/auth.js +221 -0
- package/dist/server/client-ip.d.ts +25 -0
- package/dist/server/client-ip.js +92 -0
- package/dist/server/cursor.d.ts +23 -0
- package/dist/server/cursor.js +58 -0
- package/dist/server/lifecycle.d.ts +7 -0
- package/dist/server/lifecycle.js +29 -0
- package/dist/server/mcp-http.d.ts +5 -0
- package/dist/server/mcp-http.js +199 -0
- package/dist/server/request.d.ts +33 -0
- package/dist/server/request.js +103 -0
- package/dist/server/routes/admin.d.ts +9 -0
- package/dist/server/routes/admin.js +158 -0
- package/dist/server/routes/customer-notes.d.ts +7 -0
- package/dist/server/routes/customer-notes.js +112 -0
- package/dist/server/routes/decisions.d.ts +7 -0
- package/dist/server/routes/decisions.js +133 -0
- package/dist/server/routes/incidents.d.ts +7 -0
- package/dist/server/routes/incidents.js +126 -0
- package/dist/server/routes/memories.d.ts +10 -0
- package/dist/server/routes/memories.js +179 -0
- package/dist/server/routes/policies.d.ts +8 -0
- package/dist/server/routes/policies.js +151 -0
- package/dist/server/routes/predictions.d.ts +7 -0
- package/dist/server/routes/predictions.js +164 -0
- package/dist/server/routes/processes.d.ts +7 -0
- package/dist/server/routes/processes.js +161 -0
- package/dist/server/routes/project-briefs.d.ts +8 -0
- package/dist/server/routes/project-briefs.js +136 -0
- package/dist/server/routes/recall.d.ts +8 -0
- package/dist/server/routes/recall.js +340 -0
- package/dist/server/routes/skills.d.ts +8 -0
- package/dist/server/routes/skills.js +150 -0
- package/dist/server/types.d.ts +56 -0
- package/dist/server/types.js +2 -0
- package/dist/server/validation.d.ts +14 -0
- package/dist/server/validation.js +91 -0
- package/dist/server.d.ts +7 -61
- package/dist/server.js +92 -2368
- package/dist/session-digest.d.ts +1 -1
- package/dist/session-digest.js +9 -2
- package/dist/shared.d.ts +3 -1
- package/dist/shared.js +46 -40
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +7 -5
- package/dist/stdin.js +3 -2
- package/dist/store/audit-event.d.ts +19 -0
- package/dist/store/audit-event.js +33 -0
- package/dist/store/candidates.d.ts +42 -0
- package/dist/store/candidates.js +152 -0
- package/dist/store/conflicts.d.ts +44 -0
- package/dist/store/conflicts.js +444 -0
- package/dist/store/delete-and-batch.d.ts +63 -0
- package/dist/store/delete-and-batch.js +310 -0
- package/dist/store/entry-reads.d.ts +92 -0
- package/dist/store/entry-reads.js +255 -0
- package/dist/store/entry-row.d.ts +67 -0
- package/dist/store/entry-row.js +208 -0
- package/dist/store/entry-writes.d.ts +46 -0
- package/dist/store/entry-writes.js +148 -0
- package/dist/store/handoffs.d.ts +26 -0
- package/dist/store/handoffs.js +184 -0
- package/dist/store/index-and-stats.d.ts +52 -0
- package/dist/store/index-and-stats.js +214 -0
- package/dist/store/markdown.d.ts +10 -0
- package/dist/store/markdown.js +108 -0
- package/dist/store/mirrors.d.ts +49 -0
- package/dist/store/mirrors.js +312 -0
- package/dist/store/open.d.ts +15 -0
- package/dist/store/open.js +223 -0
- package/dist/store/rows.d.ts +178 -0
- package/dist/store/rows.js +158 -0
- package/dist/store/search-rows.d.ts +86 -0
- package/dist/store/search-rows.js +254 -0
- package/dist/store/sessions.d.ts +83 -0
- package/dist/store/sessions.js +272 -0
- package/dist/store/summaries.d.ts +94 -0
- package/dist/store/summaries.js +377 -0
- package/dist/store/tenant-lookup.d.ts +12 -0
- package/dist/store/tenant-lookup.js +16 -0
- package/dist/store-cards.js +2 -1
- package/dist/summary-dirty.d.ts +4 -0
- package/dist/summary-dirty.js +32 -0
- package/dist/support-bundle.js +4 -3
- package/dist/tenant.js +2 -2
- package/dist/token-ledger.js +1 -0
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.js +16 -0
- package/dist/transcript-tail.d.ts +7 -0
- package/dist/transcript-tail.js +48 -0
- package/dist/vector-store.d.ts +27 -0
- package/dist/vector-store.js +210 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/working-memory.js +1 -1
- package/dist/yaml.js +36 -11
- package/dist-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist-ui/assets/index-DPN7cP19.js +33 -0
- package/dist-ui/assets/index-dFloKRVr.css +1 -0
- package/dist-ui/index.html +3 -25
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/dist/capture.d.ts +0 -155
- package/dist/capture.js +0 -1293
- package/dist/consolidate.d.ts +0 -58
- package/dist/consolidate.js +0 -1123
- package/dist/graph.d.ts +0 -245
- package/dist/hooks.d.ts +0 -208
- package/dist/hooks.js +0 -1073
- package/dist/importers.js +0 -897
- package/dist/predictions.js +0 -620
- package/dist/search.d.ts +0 -320
- package/dist/search.js +0 -968
- package/dist/store.d.ts +0 -744
- package/dist/store.js +0 -3372
- package/dist-ui/assets/d3-BiWEKnn4.js +0 -1
- package/dist-ui/assets/index-BhT8RvO6.js +0 -61
- package/dist-ui/assets/index-RoXXJ5dq.css +0 -1
- package/dist-ui/assets/three-BDgTxR1l.js +0 -4112
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Bearer and loopback auth for the HTTP server.
|
|
2
|
+
import { envRequireAuth } from '../env.js';
|
|
3
|
+
import { resolveTenantId } from '../tenant.js';
|
|
4
|
+
import { log } from '../log.js';
|
|
5
|
+
import { API_KEY_PREFIX, verifyApiKeyCached } from '../auth.js';
|
|
6
|
+
import { HttpError, isCrossSite, isHeaderString, LOOPBACK_HOST_HEADER } from '../http-util.js';
|
|
7
|
+
import { requestIds } from './request.js';
|
|
8
|
+
import { isJsonString } from './validation.js';
|
|
9
|
+
/**
|
|
10
|
+
* Recognise loopback remote addresses. Node reports IPv6-mapped IPv4 as
|
|
11
|
+
* '::ffff:127.0.0.1' on dual-stack sockets, so we accept that alongside
|
|
12
|
+
* the bare v4 and v6 loopbacks. Anything else is treated as remote.
|
|
13
|
+
*/
|
|
14
|
+
export function isLoopback(remoteAddress) {
|
|
15
|
+
if (!remoteAddress)
|
|
16
|
+
return false;
|
|
17
|
+
if (remoteAddress === '127.0.0.1')
|
|
18
|
+
return true;
|
|
19
|
+
if (remoteAddress === '::1')
|
|
20
|
+
return true;
|
|
21
|
+
if (remoteAddress === '::ffff:127.0.0.1')
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// A proxy on this host (nginx, Caddy, cloudflared) connects from loopback, so these headers mean the caller is not local.
|
|
26
|
+
const PROXY_HEADERS = [
|
|
27
|
+
'forwarded', 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-real-ip', 'cf-connecting-ip', 'true-client-ip',
|
|
28
|
+
];
|
|
29
|
+
// A browser on this machine is loopback too, so the no-key fallback also needs a local Host and a same-site caller.
|
|
30
|
+
function assertLocalCaller(req) {
|
|
31
|
+
if (!isLoopback(req.socket.remoteAddress))
|
|
32
|
+
throw new HttpError(401, 'auth required');
|
|
33
|
+
const proxyHeader = PROXY_HEADERS.find((name) => req.headers[name] !== undefined);
|
|
34
|
+
if (proxyHeader !== undefined) {
|
|
35
|
+
log.warn(`proxied loopback request refused: it carries ${proxyHeader}, so the no-key local fallback does not apply. ` +
|
|
36
|
+
'Send an API key (hippo auth create, then Authorization: Bearer hk_...).', { requestId: requestIds.get(req) });
|
|
37
|
+
throw new HttpError(401, 'auth required');
|
|
38
|
+
}
|
|
39
|
+
const host = req.headers.host;
|
|
40
|
+
if ((host !== undefined && !LOOPBACK_HOST_HEADER.test(host)) || isCrossSite(req)) {
|
|
41
|
+
throw new HttpError(403, 'cross-site or non-local request refused; send an API key');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function readAuthHeader(req) {
|
|
45
|
+
const raw = req.headers['authorization'];
|
|
46
|
+
if (raw === undefined)
|
|
47
|
+
return { kind: 'absent' };
|
|
48
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
49
|
+
if (!isHeaderString(value) || value.length === 0) {
|
|
50
|
+
return { kind: 'malformed' };
|
|
51
|
+
}
|
|
52
|
+
const space = value.indexOf(' ');
|
|
53
|
+
if (space < 0)
|
|
54
|
+
return { kind: 'malformed' };
|
|
55
|
+
const scheme = value.slice(0, space);
|
|
56
|
+
const token = value.slice(space + 1).trim();
|
|
57
|
+
if (scheme.toLowerCase() !== 'bearer')
|
|
58
|
+
return { kind: 'malformed' };
|
|
59
|
+
if (token.length === 0)
|
|
60
|
+
return { kind: 'malformed' };
|
|
61
|
+
return { kind: 'bearer', token };
|
|
62
|
+
}
|
|
63
|
+
// Built-in actors are the bare names below or `<name>:<detail>`; a plain prefix would also reject `clinton@corp`.
|
|
64
|
+
const RESERVED_ACTOR_NAMES = [
|
|
65
|
+
'api_key', 'localhost', 'cli', 'system', 'mcp', 'connector', 'sleep', 'post-compact', 'recall', 'agent-memories',
|
|
66
|
+
];
|
|
67
|
+
/** Add-ons call this to refuse a subject that would collide with a built-in actor. */
|
|
68
|
+
export function isReservedActor(subject) {
|
|
69
|
+
const lower = subject.toLowerCase();
|
|
70
|
+
return RESERVED_ACTOR_NAMES.some((n) => lower === n || lower.startsWith(`${n}:`));
|
|
71
|
+
}
|
|
72
|
+
function hasControlChar(s) {
|
|
73
|
+
for (let i = 0; i < s.length; i++) {
|
|
74
|
+
const c = s.charCodeAt(i);
|
|
75
|
+
if (c < 0x20 || c === 0x7f)
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/** Core owns the checks so a resolver cannot mint an actor that collides with a built-in one. */
|
|
81
|
+
function sanitiseResolved(r) {
|
|
82
|
+
// Each field is read once: a getter on plugin code could answer differently the second time.
|
|
83
|
+
const { tenantId, subject, role, scopes } = r;
|
|
84
|
+
// A resolver is plugin code, so the runtime checks hold even though the static types say string.
|
|
85
|
+
if (!isJsonString(tenantId) || tenantId.trim().length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
// Core reserves `__`-prefixed tenants (`__host__`, `__unroutable__`).
|
|
88
|
+
const tenant = tenantId.trim();
|
|
89
|
+
if (tenant.startsWith('__') || tenant.length > 256 || hasControlChar(tenant))
|
|
90
|
+
return null;
|
|
91
|
+
if (!isJsonString(subject) || subject.length < 1 || subject.length > 256)
|
|
92
|
+
return null;
|
|
93
|
+
// Padding would let "system " pass the reserved-name check yet read as `system` in an audit log.
|
|
94
|
+
if (hasControlChar(subject) || subject !== subject.trim())
|
|
95
|
+
return null;
|
|
96
|
+
if (isReservedActor(subject))
|
|
97
|
+
return null;
|
|
98
|
+
const clean = { tenantId: tenant, subject, role: role === 'admin' ? 'admin' : 'member' };
|
|
99
|
+
if (Array.isArray(scopes))
|
|
100
|
+
clean.scopes = scopes.filter((s) => isJsonString(s));
|
|
101
|
+
return clean;
|
|
102
|
+
}
|
|
103
|
+
function logResolverFailure(what, raw, token) {
|
|
104
|
+
// The plugin's message is logged, but never the token, even if the plugin echoed it.
|
|
105
|
+
log.error(`auth resolver ${what}: ${raw.split(token).join('[token]')}`);
|
|
106
|
+
}
|
|
107
|
+
/** 503 when upstream throws or misses the deadline, so a stream heartbeat can tell an outage from a revocation. */
|
|
108
|
+
async function askResolver(resolver, token, deadlineMs) {
|
|
109
|
+
let timer;
|
|
110
|
+
const deadline = new Promise((_, reject) => {
|
|
111
|
+
timer = setTimeout(() => reject(new Error(`no answer within ${deadlineMs} ms`)), deadlineMs);
|
|
112
|
+
});
|
|
113
|
+
let resolved;
|
|
114
|
+
try {
|
|
115
|
+
resolved = await Promise.race([(async () => resolver(token))(), deadline]);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
logResolverFailure('threw', err instanceof Error ? err.message : 'unknown error', token);
|
|
119
|
+
throw new HttpError(503, 'auth provider unavailable');
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
let clean = null;
|
|
125
|
+
try {
|
|
126
|
+
clean = resolved ? sanitiseResolved(resolved) : null;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// A throwing getter is a resolver bug, not an outage, so it is a 401 like any malformed answer.
|
|
130
|
+
logResolverFailure('threw', err instanceof Error ? err.message : 'unknown error', token);
|
|
131
|
+
}
|
|
132
|
+
if (!clean)
|
|
133
|
+
throw new HttpError(401, 'invalid api key');
|
|
134
|
+
return clean;
|
|
135
|
+
}
|
|
136
|
+
const DEFAULT_RESOLVER_DEADLINE_MS = 5000;
|
|
137
|
+
/** Shared by buildContextWithAuth and requireAuth so the two cannot drift. */
|
|
138
|
+
async function resolveBearer(token, opts) {
|
|
139
|
+
// Routing by shape keeps key plaintext out of plugin code and stops a resolver overriding a key's identity.
|
|
140
|
+
if (opts.authResolver && !token.startsWith(API_KEY_PREFIX)) {
|
|
141
|
+
const t = opts.authResolverTimeoutMs;
|
|
142
|
+
const deadlineMs = t !== undefined && Number.isFinite(t) && t > 0 ? t : DEFAULT_RESOLVER_DEADLINE_MS;
|
|
143
|
+
return { ...(await askResolver(opts.authResolver, token, deadlineMs)), viaAuthResolver: true };
|
|
144
|
+
}
|
|
145
|
+
const key = verifyApiKeyCached(opts.hippoRoot, token);
|
|
146
|
+
if (!key)
|
|
147
|
+
throw new HttpError(401, 'invalid api key');
|
|
148
|
+
return { tenantId: key.tenantId, subject: `api_key:${key.keyId}`, role: key.role, scopes: key.scopes };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build a per-request Context from the Authorization header and remote
|
|
152
|
+
* address. Throws HttpError(401) for invalid / missing credentials. Opens
|
|
153
|
+
* the DB only for an API-key-shaped Bearer token (or any Bearer token when no
|
|
154
|
+
* auth resolver is registered), so loopback no-auth requests stay cheap.
|
|
155
|
+
*/
|
|
156
|
+
export async function buildContextWithAuth(req, opts) {
|
|
157
|
+
const auth = readAuthHeader(req);
|
|
158
|
+
if (auth.kind === 'malformed') {
|
|
159
|
+
throw new HttpError(401, 'invalid api key');
|
|
160
|
+
}
|
|
161
|
+
if (auth.kind === 'bearer') {
|
|
162
|
+
const id = await resolveBearer(auth.token, opts);
|
|
163
|
+
const actor = { subject: id.subject, role: id.role, scopes: id.scopes };
|
|
164
|
+
if (id.viaAuthResolver)
|
|
165
|
+
actor.viaAuthResolver = true;
|
|
166
|
+
// Only the server's own tenant owns the host; any other tenant's admin key is a tenant admin.
|
|
167
|
+
else if (id.role === 'admin' && id.tenantId === resolveTenantId({}))
|
|
168
|
+
actor.hostAdmin = true;
|
|
169
|
+
return { hippoRoot: opts.hippoRoot, tenantId: id.tenantId, actor };
|
|
170
|
+
}
|
|
171
|
+
// No Authorization header. Loopback-only fallback for a direct local caller (no proxy headers),
|
|
172
|
+
// unless HIPPO_REQUIRE_AUTH=1 forbids the local-CLI escape hatch.
|
|
173
|
+
if (envRequireAuth()) {
|
|
174
|
+
throw new HttpError(401, 'auth required');
|
|
175
|
+
}
|
|
176
|
+
assertLocalCaller(req);
|
|
177
|
+
// v1.12.0: loopback fallback is process-local, treat as admin.
|
|
178
|
+
return {
|
|
179
|
+
hippoRoot: opts.hippoRoot,
|
|
180
|
+
tenantId: resolveTenantId({}),
|
|
181
|
+
actor: { subject: 'localhost:cli', role: 'admin', hostAdmin: true },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Auth check for routes that do not need a tenant Context (e.g. MCP transport,
|
|
186
|
+
* which builds its own root resolution via findHippoRoot). Throws HttpError
|
|
187
|
+
* 401 the same way buildContextWithAuth does, but skips building the Context
|
|
188
|
+
* envelope. Loopback no-auth still passes.
|
|
189
|
+
*/
|
|
190
|
+
export async function requireAuth(req, opts) {
|
|
191
|
+
const auth = readAuthHeader(req);
|
|
192
|
+
if (auth.kind === 'malformed') {
|
|
193
|
+
throw new HttpError(401, 'invalid api key');
|
|
194
|
+
}
|
|
195
|
+
if (auth.kind === 'bearer') {
|
|
196
|
+
await resolveBearer(auth.token, opts);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (envRequireAuth()) {
|
|
200
|
+
throw new HttpError(401, 'auth required');
|
|
201
|
+
}
|
|
202
|
+
assertLocalCaller(req);
|
|
203
|
+
}
|
|
204
|
+
/** Never rejects: an outage (5xx) skips one heartbeat tick, only a definite 4xx denial closes the stream. */
|
|
205
|
+
export async function heartbeatVerdict(req, opts) {
|
|
206
|
+
try {
|
|
207
|
+
await requireAuth(req, opts);
|
|
208
|
+
return 'ok';
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
return err instanceof HttpError && err.status < 500 ? 'revoked' : 'unavailable';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** Gate for any action beyond the caller's own tenant: a tenant's admin, by key or resolver, is never a host admin. */
|
|
215
|
+
export function assertCrossTenantAdmin(ctx, what) {
|
|
216
|
+
if (ctx.actor.role !== 'admin')
|
|
217
|
+
throw new HttpError(403, `${what} requires admin role`);
|
|
218
|
+
if (!ctx.actor.hostAdmin)
|
|
219
|
+
throw new HttpError(403, `${what} requires a host admin`);
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { RateLimiter } from '../rate-limit.js';
|
|
3
|
+
/**
|
|
4
|
+
* Rate-limit key for a request. Defaults to the socket's remote address.
|
|
5
|
+
*
|
|
6
|
+
* Behind a TLS-terminating proxy (Fly, most PaaS ingress) every socket
|
|
7
|
+
* carries the proxy's address, so per-IP buckets collapse into one global
|
|
8
|
+
* bucket that unauthenticated traffic can drain before auth runs. Set
|
|
9
|
+
* HIPPO_CLIENT_IP_HEADER to the header the proxy stamps with the real
|
|
10
|
+
* client address (fly-client-ip on Fly, which the edge always overwrites)
|
|
11
|
+
* to key buckets per client instead.
|
|
12
|
+
*
|
|
13
|
+
* Only set this when a trusted proxy fronts EVERY request: a directly
|
|
14
|
+
* reachable server honoring the header would let clients mint a fresh
|
|
15
|
+
* bucket per request and bypass the limiter entirely.
|
|
16
|
+
*
|
|
17
|
+
* HIPPO_TRUSTED_PROXIES (comma-separated IPs or CIDRs) pins which peers count
|
|
18
|
+
* as that proxy: the header is read only when the socket peer is listed, and
|
|
19
|
+
* listed hops are skipped. In a comma-joined chain (X-Forwarded-For) the key is
|
|
20
|
+
* the rightmost hop no trusted proxy added; entries to its left are whatever
|
|
21
|
+
* the client sent, so they never pick the bucket.
|
|
22
|
+
*/
|
|
23
|
+
export declare function clientIpForRateLimit(req: IncomingMessage): string;
|
|
24
|
+
export declare function enforceRateLimit(req: IncomingMessage, path: string, limiter?: RateLimiter): void;
|
|
25
|
+
//# sourceMappingURL=client-ip.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Client IP keying and the per-IP rate limit for /v1 and /mcp.
|
|
2
|
+
import { envClientIpHeader, envTrustedProxies } from '../env.js';
|
|
3
|
+
import { BlockList, isIP } from 'node:net';
|
|
4
|
+
import { log } from '../log.js';
|
|
5
|
+
import { HttpError } from '../http-util.js';
|
|
6
|
+
/**
|
|
7
|
+
* Rate-limit key for a request. Defaults to the socket's remote address.
|
|
8
|
+
*
|
|
9
|
+
* Behind a TLS-terminating proxy (Fly, most PaaS ingress) every socket
|
|
10
|
+
* carries the proxy's address, so per-IP buckets collapse into one global
|
|
11
|
+
* bucket that unauthenticated traffic can drain before auth runs. Set
|
|
12
|
+
* HIPPO_CLIENT_IP_HEADER to the header the proxy stamps with the real
|
|
13
|
+
* client address (fly-client-ip on Fly, which the edge always overwrites)
|
|
14
|
+
* to key buckets per client instead.
|
|
15
|
+
*
|
|
16
|
+
* Only set this when a trusted proxy fronts EVERY request: a directly
|
|
17
|
+
* reachable server honoring the header would let clients mint a fresh
|
|
18
|
+
* bucket per request and bypass the limiter entirely.
|
|
19
|
+
*
|
|
20
|
+
* HIPPO_TRUSTED_PROXIES (comma-separated IPs or CIDRs) pins which peers count
|
|
21
|
+
* as that proxy: the header is read only when the socket peer is listed, and
|
|
22
|
+
* listed hops are skipped. In a comma-joined chain (X-Forwarded-For) the key is
|
|
23
|
+
* the rightmost hop no trusted proxy added; entries to its left are whatever
|
|
24
|
+
* the client sent, so they never pick the bucket.
|
|
25
|
+
*/
|
|
26
|
+
export function clientIpForRateLimit(req) {
|
|
27
|
+
const socketIp = req.socket.remoteAddress ?? 'unknown';
|
|
28
|
+
const header = envClientIpHeader();
|
|
29
|
+
if (!header)
|
|
30
|
+
return socketIp;
|
|
31
|
+
const trusted = trustedProxyList(envTrustedProxies());
|
|
32
|
+
if (trusted && !isTrustedProxy(trusted, socketIp))
|
|
33
|
+
return socketIp;
|
|
34
|
+
const raw = req.headers[header];
|
|
35
|
+
const hops = (Array.isArray(raw) ? raw.join(',') : raw ?? '')
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((hop) => hop.trim())
|
|
38
|
+
.filter((hop) => hop.length > 0);
|
|
39
|
+
for (let i = hops.length - 1; i >= 0; i--) {
|
|
40
|
+
if (!trusted || !isTrustedProxy(trusted, hops[i]))
|
|
41
|
+
return hops[i];
|
|
42
|
+
}
|
|
43
|
+
return hops[0] ?? socketIp;
|
|
44
|
+
}
|
|
45
|
+
let trustedProxyCache;
|
|
46
|
+
/** Parses HIPPO_TRUSTED_PROXIES once per distinct value; undefined when unset, so any peer may set the header. */
|
|
47
|
+
function trustedProxyList(raw) {
|
|
48
|
+
if (!raw?.trim())
|
|
49
|
+
return undefined;
|
|
50
|
+
if (trustedProxyCache?.raw === raw)
|
|
51
|
+
return trustedProxyCache.list;
|
|
52
|
+
const list = new BlockList();
|
|
53
|
+
for (const entry of raw.split(',').map((e) => e.trim()).filter((e) => e.length > 0)) {
|
|
54
|
+
const [addr = '', prefix] = entry.split('/');
|
|
55
|
+
const family = isIP(addr) === 6 ? 'ipv6' : 'ipv4';
|
|
56
|
+
const bits = prefix === undefined ? undefined : Number(prefix);
|
|
57
|
+
const maxBits = family === 'ipv6' ? 128 : 32;
|
|
58
|
+
if (isIP(addr) === 0 || (bits !== undefined && (!Number.isInteger(bits) || bits < 0 || bits > maxBits))) {
|
|
59
|
+
log.warn(`HIPPO_TRUSTED_PROXIES: ignoring '${entry}', not an IP address or CIDR`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (bits === undefined)
|
|
63
|
+
list.addAddress(addr, family);
|
|
64
|
+
else
|
|
65
|
+
list.addSubnet(addr, bits, family);
|
|
66
|
+
}
|
|
67
|
+
trustedProxyCache = { raw, list };
|
|
68
|
+
return list;
|
|
69
|
+
}
|
|
70
|
+
function isTrustedProxy(list, ip) {
|
|
71
|
+
const bare = ip.startsWith('::ffff:') && isIP(ip.slice(7)) === 4 ? ip.slice(7) : ip;
|
|
72
|
+
const family = isIP(bare);
|
|
73
|
+
return family !== 0 && list.check(bare, family === 6 ? 'ipv6' : 'ipv4');
|
|
74
|
+
}
|
|
75
|
+
export function enforceRateLimit(req, path, limiter) {
|
|
76
|
+
// E3: per-IP rate limit on /v1/* and /mcp* to bound api-key-id enumeration. /health
|
|
77
|
+
// (a liveness probe) and other paths are never throttled. A 429 thrown
|
|
78
|
+
// here lands in the createServer catch like any other HttpError.
|
|
79
|
+
//
|
|
80
|
+
// Keyed on the socket's remote address by default. Behind a TLS-terminating
|
|
81
|
+
// proxy every socket carries the proxy's address, collapsing the per-IP
|
|
82
|
+
// buckets into one global bucket that pre-auth traffic can drain; set
|
|
83
|
+
// HIPPO_CLIENT_IP_HEADER there so each real client gets its own bucket
|
|
84
|
+
// (see clientIpForRateLimit).
|
|
85
|
+
if (limiter && (path.startsWith('/v1/') || path === '/mcp' || path === '/mcp/stream')) {
|
|
86
|
+
const ip = clientIpForRateLimit(req);
|
|
87
|
+
if (!limiter.check(ip)) {
|
|
88
|
+
throw new HttpError(429, 'rate limit exceeded');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=client-ip.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
|
+
import type { KeysetPosition } from '../keyset.js';
|
|
3
|
+
type CursorPart = 'string' | 'integer';
|
|
4
|
+
/** Bare-array routes cannot grow a body field without breaking clients, so their next cursor rides this header. */
|
|
5
|
+
export declare const NEXT_CURSOR_HEADER = "X-Next-Cursor";
|
|
6
|
+
export declare function encodeCursor(pos: KeysetPosition): string;
|
|
7
|
+
/** The position a `?cursor=` names, or undefined when the param is absent; anything this server did not mint is a 400. */
|
|
8
|
+
export declare function parseCursor(raw: string | null, keyPart: CursorPart, idPart: CursorPart): KeysetPosition | undefined;
|
|
9
|
+
export interface Page<T> {
|
|
10
|
+
items: T[];
|
|
11
|
+
nextCursor: string | null;
|
|
12
|
+
}
|
|
13
|
+
/** Drops the one-row lookahead the query fetched past `limit` and names the next page's cursor, null on the last page. */
|
|
14
|
+
export declare function pageOf<T>(rows: readonly T[], limit: number, positionOf: (row: T) => KeysetPosition): Page<T>;
|
|
15
|
+
/** Rows ordered by (created_at DESC, id DESC): the decision, incident, process, policy, skill, brief, note and prediction tables. */
|
|
16
|
+
export declare function byCreatedAt(row: {
|
|
17
|
+
createdAt: string;
|
|
18
|
+
id: number;
|
|
19
|
+
}): KeysetPosition;
|
|
20
|
+
/** Sets the next-page header on a bare-array response; absent means this was the last page. */
|
|
21
|
+
export declare function setNextCursorHeader(res: ServerResponse, nextCursor: string | null): void;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { HttpError } from '../http-util.js';
|
|
2
|
+
import { isJsonNumber, isJsonString } from './validation.js';
|
|
3
|
+
/** Bare-array routes cannot grow a body field without breaking clients, so their next cursor rides this header. */
|
|
4
|
+
export const NEXT_CURSOR_HEADER = 'X-Next-Cursor';
|
|
5
|
+
const MAX_CURSOR_CHARS = 1024;
|
|
6
|
+
const CURSOR_RE = /^[A-Za-z0-9_-]+$/;
|
|
7
|
+
export function encodeCursor(pos) {
|
|
8
|
+
return Buffer.from(JSON.stringify([pos.key, pos.id])).toString('base64url');
|
|
9
|
+
}
|
|
10
|
+
function decodeJson(raw) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
if (e instanceof SyntaxError)
|
|
17
|
+
return undefined;
|
|
18
|
+
throw e;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isPart(value, part) {
|
|
22
|
+
if (part === 'string')
|
|
23
|
+
return isJsonString(value) && value.length > 0;
|
|
24
|
+
return isJsonNumber(value) && Number.isInteger(value);
|
|
25
|
+
}
|
|
26
|
+
function toPosition(parsed, keyPart, idPart) {
|
|
27
|
+
if (!Array.isArray(parsed) || parsed.length !== 2)
|
|
28
|
+
return undefined;
|
|
29
|
+
const [key, id] = parsed;
|
|
30
|
+
return isPart(key, keyPart) && isPart(id, idPart) ? { key, id } : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** The position a `?cursor=` names, or undefined when the param is absent; anything this server did not mint is a 400. */
|
|
33
|
+
export function parseCursor(raw, keyPart, idPart) {
|
|
34
|
+
if (raw === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
const parsed = raw.length <= MAX_CURSOR_CHARS && CURSOR_RE.test(raw) ? decodeJson(raw) : undefined;
|
|
37
|
+
const pos = toPosition(parsed, keyPart, idPart);
|
|
38
|
+
if (!pos)
|
|
39
|
+
throw new HttpError(400, 'cursor is malformed; pass next_cursor from the previous page unchanged');
|
|
40
|
+
return pos;
|
|
41
|
+
}
|
|
42
|
+
/** Drops the one-row lookahead the query fetched past `limit` and names the next page's cursor, null on the last page. */
|
|
43
|
+
export function pageOf(rows, limit, positionOf) {
|
|
44
|
+
if (rows.length <= limit)
|
|
45
|
+
return { items: [...rows], nextCursor: null };
|
|
46
|
+
const items = rows.slice(0, limit);
|
|
47
|
+
return { items, nextCursor: encodeCursor(positionOf(items[items.length - 1])) };
|
|
48
|
+
}
|
|
49
|
+
/** Rows ordered by (created_at DESC, id DESC): the decision, incident, process, policy, skill, brief, note and prediction tables. */
|
|
50
|
+
export function byCreatedAt(row) {
|
|
51
|
+
return { key: row.createdAt, id: row.id };
|
|
52
|
+
}
|
|
53
|
+
/** Sets the next-page header on a bare-array response; absent means this was the last page. */
|
|
54
|
+
export function setNextCursorHeader(res, nextCursor) {
|
|
55
|
+
if (nextCursor !== null)
|
|
56
|
+
res.setHeader(NEXT_CURSOR_HEADER, nextCursor);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Server, ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Stop accepting, end streams at once (they never finish on their own), give other in-flight
|
|
4
|
+
* requests up to `drainMs`, then close whatever is left.
|
|
5
|
+
*/
|
|
6
|
+
export declare function drainAndClose(server: Server, inflight: ReadonlySet<ServerResponse>, drainMs: number): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { log } from '../log.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stop accepting, end streams at once (they never finish on their own), give other in-flight
|
|
4
|
+
* requests up to `drainMs`, then close whatever is left.
|
|
5
|
+
*/
|
|
6
|
+
export async function drainAndClose(server, inflight, drainMs) {
|
|
7
|
+
const closed = new Promise((resolve, reject) => {
|
|
8
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
9
|
+
});
|
|
10
|
+
server.closeIdleConnections?.();
|
|
11
|
+
for (const res of inflight) {
|
|
12
|
+
if (res.headersSent && !res.writableEnded)
|
|
13
|
+
res.destroy();
|
|
14
|
+
else if (!res.headersSent)
|
|
15
|
+
res.setHeader('Connection', 'close');
|
|
16
|
+
}
|
|
17
|
+
let timer;
|
|
18
|
+
const timedOut = new Promise((resolve) => {
|
|
19
|
+
timer = setTimeout(() => resolve(true), drainMs);
|
|
20
|
+
});
|
|
21
|
+
const late = await Promise.race([closed.then(() => false), timedOut]);
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
if (late) {
|
|
24
|
+
log.warn(`shutdown: ${inflight.size} request(s) still running after ${drainMs} ms; closing them`);
|
|
25
|
+
server.closeAllConnections?.();
|
|
26
|
+
}
|
|
27
|
+
await closed;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { ServeOpts } from './types.js';
|
|
3
|
+
export declare function handleMcpPost(req: IncomingMessage, res: ServerResponse, opts: ServeOpts): Promise<void>;
|
|
4
|
+
export declare function handleMcpStream(req: IncomingMessage, res: ServerResponse, opts: ServeOpts, streamSlots: Map<string, number>): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=mcp-http.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// MCP over HTTP: POST /mcp and the GET /mcp/stream SSE keepalive.
|
|
2
|
+
import { envMcpSseHeartbeatMs, envMcpSseMaxAgeSec, envMcpSseMaxStreams } from '../env.js';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { handleMcpRequest, mcpErrorResponse } from '../mcp/server.js';
|
|
5
|
+
import { HttpError, isJsonObjectRecord, readBody, sendJson } from '../http-util.js';
|
|
6
|
+
import { buildContextWithAuth, heartbeatVerdict, readAuthHeader, requireAuth } from './auth.js';
|
|
7
|
+
import { clientIpForRateLimit } from './client-ip.js';
|
|
8
|
+
import { requestIds } from './request.js';
|
|
9
|
+
import { isJsonString } from './validation.js';
|
|
10
|
+
/**
|
|
11
|
+
* Build a per-client key for MCP state isolation under HTTP-MCP. Used by
|
|
12
|
+
* mcp/server.ts to scope `lastRecalledIds` to the calling client so two
|
|
13
|
+
* clients on the same tenant cannot poison each other's outcome feedback.
|
|
14
|
+
*
|
|
15
|
+
* Token is hashed (sha256, 16-hex-char prefix) so we never log or persist
|
|
16
|
+
* the raw bearer. Combined with remoteAddress so two clients sharing a key
|
|
17
|
+
* (e.g. on a shared Postman environment) are still separable in the common
|
|
18
|
+
* case. 'noauth' covers loopback no-auth and is acceptable because that
|
|
19
|
+
* path is single-host single-user.
|
|
20
|
+
*/
|
|
21
|
+
function buildMcpClientKey(req) {
|
|
22
|
+
const auth = readAuthHeader(req);
|
|
23
|
+
const tokenHash = auth.kind === 'bearer'
|
|
24
|
+
? createHash('sha256').update(auth.token).digest('hex').slice(0, 16)
|
|
25
|
+
: 'noauth';
|
|
26
|
+
const addr = req.socket.remoteAddress ?? 'unknown';
|
|
27
|
+
return `http:${tokenHash}:${addr}`;
|
|
28
|
+
}
|
|
29
|
+
// ── MCP-over-HTTP/SSE transport (Task 11) ──
|
|
30
|
+
//
|
|
31
|
+
// Two routes implement an MCP HTTP transport alongside the stdio one. Both
|
|
32
|
+
// dispatch to the same `handleMcpRequest` as the stdio loop in src/mcp/server.ts.
|
|
33
|
+
//
|
|
34
|
+
// POST /mcp — Send a JSON-RPC request, get a JSON-RPC response synchronously
|
|
35
|
+
// in the body. Content-type: application/json both ways.
|
|
36
|
+
// GET /mcp/stream — Open an SSE stream for server-initiated messages.
|
|
37
|
+
// v1 simplification: this stream is keepalive-only. Clients
|
|
38
|
+
// that need server-pushed notifications/progress will see
|
|
39
|
+
// only `: ping` comments every 30s. All real responses come
|
|
40
|
+
// back synchronously on POST /mcp. This matches the
|
|
41
|
+
// "synchronous JSON in body" leg of the MCP HTTP spec and
|
|
42
|
+
// is enough for `tools/list` / `tools/call` round-trips.
|
|
43
|
+
// Server-initiated SSE messages will be wired in a later task.
|
|
44
|
+
//
|
|
45
|
+
// Auth: same as /v1/* — Bearer token validated via `requireAuth`, with the
|
|
46
|
+
// loopback no-auth fallback. SSE check runs once at stream-open.
|
|
47
|
+
export async function handleMcpPost(req, res, opts) {
|
|
48
|
+
// Build the same Context the /v1/* routes use so MCP tool calls inherit
|
|
49
|
+
// the server's bound hippoRoot and the auth-resolved tenantId / actor.
|
|
50
|
+
// Without this, executeTool would walk from cwd via findHippoRoot() and
|
|
51
|
+
// pull tenant from HIPPO_TENANT, dropping a valid Bearer for tenant B
|
|
52
|
+
// back to whatever the env says.
|
|
53
|
+
const ctx = await buildContextWithAuth(req, opts);
|
|
54
|
+
const raw = await readBody(req);
|
|
55
|
+
let mcpReq;
|
|
56
|
+
try {
|
|
57
|
+
mcpReq = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new HttpError(400, 'invalid JSON-RPC body');
|
|
61
|
+
}
|
|
62
|
+
if (!isJsonObjectRecord(mcpReq) || !isJsonString(mcpReq.method)) {
|
|
63
|
+
throw new HttpError(400, 'JSON-RPC body must include a method string');
|
|
64
|
+
}
|
|
65
|
+
// SAFETY: validated above as a plain JSON object carrying a string method;
|
|
66
|
+
// the remaining McpRequest wire fields (jsonrpc, id, params) are checked or
|
|
67
|
+
// safely defaulted inside handleMcpRequest's JSON-RPC dispatch.
|
|
68
|
+
const rpcReq = mcpReq;
|
|
69
|
+
let mcpRes;
|
|
70
|
+
try {
|
|
71
|
+
mcpRes = await handleMcpRequest(rpcReq, {
|
|
72
|
+
hippoRoot: ctx.hippoRoot,
|
|
73
|
+
tenantId: ctx.tenantId,
|
|
74
|
+
// v1.12.0: McpContext.actor stays string; extract subject at the boundary.
|
|
75
|
+
actor: ctx.actor.subject,
|
|
76
|
+
// The caller's real role: MCP tools must not run a member key as admin.
|
|
77
|
+
role: ctx.actor.role,
|
|
78
|
+
scopes: ctx.actor.scopes,
|
|
79
|
+
viaAuthResolver: ctx.actor.viaAuthResolver,
|
|
80
|
+
hostAdmin: ctx.actor.hostAdmin,
|
|
81
|
+
clientKey: buildMcpClientKey(req),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
mcpRes = mcpErrorResponse(rpcReq.id, err, requestIds.get(req));
|
|
86
|
+
}
|
|
87
|
+
if (mcpRes === null) {
|
|
88
|
+
// Notification — no body, 202 Accepted.
|
|
89
|
+
res.writeHead(202);
|
|
90
|
+
res.end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
sendJson(res, 200, mcpRes);
|
|
94
|
+
}
|
|
95
|
+
// Each open stream holds a socket and a timer, so one key (or one IP when keyless) gets a bounded number.
|
|
96
|
+
const DEFAULT_MAX_STREAMS_PER_CLIENT = 8;
|
|
97
|
+
/** The bucket a stream counts against: a hash of the bearer token, else the client IP. */
|
|
98
|
+
function streamSlotKey(req) {
|
|
99
|
+
const auth = readAuthHeader(req);
|
|
100
|
+
if (auth.kind === 'bearer')
|
|
101
|
+
return `key:${createHash('sha256').update(auth.token).digest('hex').slice(0, 16)}`;
|
|
102
|
+
return `ip:${clientIpForRateLimit(req)}`;
|
|
103
|
+
}
|
|
104
|
+
/** Takes a stream slot or throws 429; the slot is released once, when the response closes. */
|
|
105
|
+
function acquireStreamSlot(req, res, slots) {
|
|
106
|
+
const max = envMcpSseMaxStreams() ?? DEFAULT_MAX_STREAMS_PER_CLIENT;
|
|
107
|
+
const key = streamSlotKey(req);
|
|
108
|
+
const open = slots.get(key) ?? 0;
|
|
109
|
+
if (open >= max)
|
|
110
|
+
throw new HttpError(429, `too many open streams for this client (limit ${max}); close one first`);
|
|
111
|
+
slots.set(key, open + 1);
|
|
112
|
+
res.once('close', () => {
|
|
113
|
+
const left = (slots.get(key) ?? 1) - 1;
|
|
114
|
+
if (left > 0)
|
|
115
|
+
slots.set(key, left);
|
|
116
|
+
else
|
|
117
|
+
slots.delete(key);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function handleMcpStream(req, res, opts, streamSlots) {
|
|
121
|
+
await requireAuth(req, opts);
|
|
122
|
+
// An async resolver can outlive the client; 'close' has already fired, so no timer may start.
|
|
123
|
+
if (req.destroyed || res.destroyed || req.socket.destroyed)
|
|
124
|
+
return;
|
|
125
|
+
acquireStreamSlot(req, res, streamSlots);
|
|
126
|
+
res.writeHead(200, {
|
|
127
|
+
'content-type': 'text/event-stream',
|
|
128
|
+
'cache-control': 'no-cache',
|
|
129
|
+
connection: 'keep-alive',
|
|
130
|
+
});
|
|
131
|
+
// Initial ping so smoke tests can confirm the stream is live without
|
|
132
|
+
// waiting for the first keepalive interval.
|
|
133
|
+
res.write(': ping\n\n');
|
|
134
|
+
// v0.39 SSE hardening:
|
|
135
|
+
// - Heartbeat re-validates the bearer (default 60s). If the key was
|
|
136
|
+
// revoked or rotated, close the stream with reason='auth_revoked'.
|
|
137
|
+
// - MCP_SSE_MAX_AGE_SEC (default 3600) caps stream lifetime; close
|
|
138
|
+
// with reason='max_age_exceeded' when reached.
|
|
139
|
+
// - MCP_SSE_HEARTBEAT_MS (default 60000) lets tests run with a short
|
|
140
|
+
// interval without waiting a full minute.
|
|
141
|
+
const heartbeatMs = envMcpSseHeartbeatMs() ?? 60000;
|
|
142
|
+
const maxAgeMs = (envMcpSseMaxAgeSec() ?? 3600) * 1000;
|
|
143
|
+
const startedAt = Date.now();
|
|
144
|
+
let closed = false;
|
|
145
|
+
let checking = false;
|
|
146
|
+
const closeWith = (reason) => {
|
|
147
|
+
if (closed)
|
|
148
|
+
return;
|
|
149
|
+
closed = true;
|
|
150
|
+
try {
|
|
151
|
+
res.write(`event: closed\ndata: ${JSON.stringify({ reason })}\n\n`);
|
|
152
|
+
}
|
|
153
|
+
catch { /* socket already gone */ }
|
|
154
|
+
try {
|
|
155
|
+
res.end();
|
|
156
|
+
}
|
|
157
|
+
catch { /* socket already gone */ }
|
|
158
|
+
};
|
|
159
|
+
const ping = setInterval(() => {
|
|
160
|
+
if (closed) {
|
|
161
|
+
clearInterval(ping);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (Date.now() - startedAt >= maxAgeMs) {
|
|
165
|
+
closeWith('max_age_exceeded');
|
|
166
|
+
clearInterval(ping);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (checking)
|
|
170
|
+
return;
|
|
171
|
+
checking = true;
|
|
172
|
+
void heartbeatVerdict(req, opts).then((verdict) => {
|
|
173
|
+
checking = false;
|
|
174
|
+
if (closed || verdict === 'unavailable')
|
|
175
|
+
return;
|
|
176
|
+
if (verdict === 'revoked') {
|
|
177
|
+
closeWith('auth_revoked');
|
|
178
|
+
clearInterval(ping);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
res.write(': ping\n\n');
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
clearInterval(ping); // the client hung up; stop pinging a dead socket
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}, heartbeatMs);
|
|
189
|
+
// Don't keep the event loop alive just for this timer — the server's
|
|
190
|
+
// listener already does that, and tests want the process to exit cleanly.
|
|
191
|
+
if (ping.unref instanceof Function)
|
|
192
|
+
ping.unref();
|
|
193
|
+
// res 'close' covers an early socket drop that req 'close' can miss.
|
|
194
|
+
res.on('close', () => {
|
|
195
|
+
closed = true;
|
|
196
|
+
clearInterval(ping);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=mcp-http.js.map
|