From 091f6dac5ced180e4eee05fc9e27c6cefe61f1ea Mon Sep 17 00:00:00 2001
From: Ralph Alexander Bariz <ralph.bariz@pm.me>
Date: Sat, 18 Jan 2025 11:09:58 +0100
Subject: [PATCH] - introduced keeper concept - introduced bouncer keeper -
 fixes

---
 CMakeLists.txt                  |   8 +-
 include/causal/core/aspect.hpp  |   4 +-
 include/causal/core/wirks.hpp   |   5 +-
 include/causal/data/channel.hpp |  96 ++++++++------
 include/causal/data/crypto.hpp  |   2 +-
 include/causal/data/keeper.hpp  | 219 ++++++++++++++++++++++++++++++++
 include/causal/data/memory.hpp  |  21 +--
 test/data/channel.cpp           |  26 ++--
 test/data/keeper.cpp            | 104 +++++++++++++++
 9 files changed, 416 insertions(+), 69 deletions(-)
 create mode 100644 include/causal/data/keeper.hpp
 create mode 100644 test/data/keeper.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index f6a0ac8d..440144b0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -110,12 +110,14 @@ add_library(causal-data src/data/junction.cpp
                         src/data/fs.cpp)
 target_link_libraries(causal-data causal-core ${GnuTLS_LIBRARIES})
 install(FILES include/causal/data/channel.hpp
+              include/causal/data/crypto.hpp
+              include/causal/data/keeper.hpp
               include/causal/data/junction.hpp
               include/causal/data/memory.hpp
               include/causal/data/fs.hpp
-        COMPONENT core-dev
+        COMPONENT data-dev
         DESTINATION include/causal/data)
-install(TARGETS causal-data COMPONENT core DESTINATION lib)
+install(TARGETS causal-data COMPONENT data DESTINATION lib)
 
 find_package(Msgpack)
 if(Msgpack_FOUND)
@@ -196,7 +198,6 @@ if(Msgpack_FOUND AND OpenDHT_FOUND)
         ${OpenDHT_INCLUDE_DIRS}
     )
     install(FILES include/causal/data/opendht.hpp
-                  include/causal/data/crypto.hpp
             COMPONENT data-source-opendht-dev
             DESTINATION include/causal/data)
     install(TARGETS causal-data-opendht COMPONENT data-source-opendht DESTINATION lib)
@@ -292,6 +293,7 @@ if(GTEST_FOUND)
     if(Msgpack_FOUND)
         add_causal_test(NAME test_causal-data_msgpack SOURCES test/data/msgpack.cpp LIBS causal-msgpack)
         add_causal_test(NAME test_causal-data_memory SOURCES test/data/memory.cpp LIBS causal-data causal-msgpack)
+        add_causal_test(NAME test_causal-data_keeper SOURCES test/data/keeper.cpp LIBS causal-data causal-msgpack)
         add_causal_test(NAME test_causal-data_fs SOURCES test/data/fs.cpp LIBS causal-data causal-msgpack)
     endif()
 
diff --git a/include/causal/core/aspect.hpp b/include/causal/core/aspect.hpp
index 29b50117..27688f9b 100644
--- a/include/causal/core/aspect.hpp
+++ b/include/causal/core/aspect.hpp
@@ -217,7 +217,7 @@ namespace causal {
             /** @brief dispose an aspect identified by given id from space
              * @param id of aspect (id's of types incompatible with space are ignored)
              */
-            virtual void dispose(const std::any id) noexcept {}
+            virtual void dispose(const std::any id) noexcept = 0;
 
             /** @brief destroy an aspect's local representation
              * @param id of aspect (id's of types incompatible with space are ignored)
@@ -792,7 +792,7 @@ namespace causal {
         EXCEPTION_ERROR(facet_active, "trying to modify an active facet")
 
         /// @brief exception thrown when trying to use an invalid facet
-        EXCEPTION_ERROR(facet_invalid, "trying to use an ivalid facet")
+        EXCEPTION_ERROR(facet_invalid, "trying to use an invalid facet")
 
         /// @brief abstract base for facets of aspect(s)
         class facet : public focusable, public borrowable {};
diff --git a/include/causal/core/wirks.hpp b/include/causal/core/wirks.hpp
index 48e49152..7e8a541c 100644
--- a/include/causal/core/wirks.hpp
+++ b/include/causal/core/wirks.hpp
@@ -637,11 +637,10 @@ namespace causal {
          */
         template<typename... MsgT>
         class vortex {
-        private:
+        protected:
             /// @brief branch to trigger wirks in
             const std::shared_ptr<branch> brnch;
-            
-        protected:
+
             /// @brief mutex synchronizing vortex inteactions
             mutable std::shared_mutex mtx;
 
diff --git a/include/causal/data/channel.hpp b/include/causal/data/channel.hpp
index 562fa824..1ed8f4dd 100644
--- a/include/causal/data/channel.hpp
+++ b/include/causal/data/channel.hpp
@@ -8,9 +8,12 @@
 #include <set>
 #include <shared_mutex>
 #include <functional>
+#include <type_traits>
+#include <unordered_map>
 
 #include "causal/core/aspect.hpp"
 #include "causal/core/wirks.hpp"
+#include "causal/trace.hpp"
 
 #ifndef _DATA_WRAP_SESSION_SIZE
 #define _DATA_WRAP_SESSION_SIZE 64
@@ -63,31 +66,6 @@ namespace causal {
             std::string session, peer;
         };
 
-        struct IdentifyRequest {
-            std::string session;
-            std::string id;
-            std::string key;
-        };
-        struct AnonRequest {
-            std::string session;
-        };
-        struct IdentifyResponse {
-            std::string session;
-            std::string id;
-            std::string sign;
-        };
-
-        struct Identified {
-            std::string session, peer;
-            std::string id;
-            std::string sign;
-            std::shared_ptr<source<IdSessionMeta>> src;
-        };
-        struct SignedOut {
-            std::string session, peer;
-            std::string id;
-        };
-
         /** @brief abstract base of channel
          * @tparam T message data type
          * @tparam M metadata type attached to messages
@@ -120,12 +98,11 @@ namespace causal {
         class sourced_channel : public channel<std::string, M> {
             friend class source<M>;
         protected:
-            const std::shared_ptr<source<M>> src;
-
             virtual void push_pipe(const std::string msg) const {
                 if(this->src)
                     this->src->push(msg);
             }
+
             sourced_channel(std::shared_ptr<source<M>> s)
             : src(std::move(s)) {
                 ASSERT_MSG(scope_data, this->src, "trying to create channel without valid source")
@@ -133,6 +110,8 @@ namespace causal {
             }
 
         public:
+            const std::shared_ptr<source<M>> src;
+
             virtual ~sourced_channel() {
                 this->src->remove_channel(*this);
             }
@@ -146,18 +125,10 @@ namespace causal {
         protected:
             mutable std::shared_mutex mtx;
             std::set<const sourced_channel<M>*> channels;
-            
-            /** @brief pulls a message from source to registered channels
-             * @param msg copy of message data
-             * @param meta copy of message metadata
-             */
-            void pull(const std::string msg, const M meta) const {
-                std::shared_lock l(this->mtx);
-                for(auto c : this->channels)
-                    c->pull(msg, meta);
-            }
 
         public:
+            using metaT = M;
+
             virtual ~source() {
                 std::unique_lock l(this->mtx);
                 ASSERT_MSG(scope_data, this->channels.empty(), "destroying junction while channels are connected to it")
@@ -180,6 +151,16 @@ namespace causal {
                 std::unique_lock l(this->mtx);
                 ASSERT_MSG(scope_data, this->channels.erase(&c), "channel is not registered for source")
             }
+            
+            /** @brief pulls a message from source to registered channels
+             * @param msg copy of message data
+             * @param meta copy of message metadata
+             */
+            void pull(const std::string msg, const M meta) const {
+                std::shared_lock l(this->mtx);
+                for(auto c : this->channels)
+                    c->pull(msg, meta);
+            }
 
             /** @brief pushes message to source
              * @param msg lvalue reference to message
@@ -281,11 +262,51 @@ namespace causal {
             }
         };
 
+        template<typename SM, typename M=NoMeta>
+        class wrapping_source final : public source<M> {
+            static_assert(std::is_base_of<SM, M>::value || !std::is_same<SM, M>::value, "a wrapping source can only use metadata derriving from inner source's metadata");
+        protected:
+            class channel final : public sourced_channel<SM> {
+            protected:
+                virtual void pull(const std::string d, const SM meta) const override {
+                    this->_parent->pull(d, this->_parent->meta);
+                }
+
+            public:
+                const wrapping_source<SM, M> *const _parent;
+
+                channel(const wrapping_source<SM, M> *const p, std::shared_ptr<source<SM>> s) : sourced_channel<SM>(s), _parent(p) {}
+            };
+
+            channel inner;
+
+            wrapping_source(std::shared_ptr<source<SM>> i, M m)
+            : inner(this, i), meta(m) {}
+
+        public:
+            static std::shared_ptr<wrapping_source<SM, M>> make(std::shared_ptr<source<SM>> inner, M meta = {}) {
+                return std::shared_ptr<wrapping_source<SM, M>>(new wrapping_source<SM, M>(inner, meta));
+            }
+        
+            const M meta;
+
+            virtual ~wrapping_source() = default;
+
+            virtual void push(const std::string& msg) const override {
+                this->inner.src->pull(msg, this->meta);
+            }
+
+            virtual void push(std::string&& msg) const override {
+                this->inner.src->pull(std::forward<std::string>(msg), this->meta);
+            }
+        };
+
         template<typename... MsgT>
         class gate : public cc::vortex<SessionCreated, DisposeSession, SessionDisposed, MsgT...> {
         protected:
             gate(std::shared_ptr<cc::branch> b)
             : cc::vortex<SessionCreated, DisposeSession, SessionDisposed, MsgT...>(b) {};
+            virtual ~gate() = default;
         };
 
         /// @brief loopback gate
@@ -329,7 +350,6 @@ namespace causal {
              * @param j junction to expose
              */
             static std::shared_ptr<wrap> make(std::shared_ptr<cc::branch> b);
-
             ~wrap() = default;
 
             const std::pair<std::string, std::shared_ptr<source<metaT>>> forge();
diff --git a/include/causal/data/crypto.hpp b/include/causal/data/crypto.hpp
index 95d5b56f..e80d2f72 100644
--- a/include/causal/data/crypto.hpp
+++ b/include/causal/data/crypto.hpp
@@ -123,7 +123,7 @@ namespace causal {
             static void init();
         };
 
-        std::string gnutls_derrive_sha_key(const std::string& password, u_short key_length);
+        std::string gnutls_derrive_key_sha(const std::string& password, u_short key_length);
 
         EXCEPTION_ERROR(sha_hash_lenght_invalid, "hash_length should be larger than or equal to 256")
 
diff --git a/include/causal/data/keeper.hpp b/include/causal/data/keeper.hpp
new file mode 100644
index 00000000..62919dac
--- /dev/null
+++ b/include/causal/data/keeper.hpp
@@ -0,0 +1,219 @@
+// Licensed under the terms of the AGPL-3.0-only (GNU AFFERO GENERAL PUBLIC LICENSE version 3)
+// Copyright (C) 2018-2024 by Ralph Alexander Bariz
+
+#pragma once
+
+#include <msgpack/adaptor/define_decl.hpp>
+#include <mutex>
+#include <memory>
+#include <set>
+#include <shared_mutex>
+#include <functional>
+#include <type_traits>
+#include <unordered_map>
+
+#include "causal/core/aspect.hpp"
+#include "causal/core/wirks.hpp"
+#include "causal/data/channel.hpp"
+#include "causal/data/crypto.hpp"
+#include "causal/data/msgpack.hpp"
+#include "causal/trace.hpp"
+
+#ifndef _DATA_WRAP_SESSION_SIZE
+#define _DATA_WRAP_SESSION_SIZE 64
+#endif
+
+namespace causal {
+    namespace data {
+        namespace cc = causal::core;
+
+        struct IdentifyRequest {
+            std::string session;
+            std::string id;
+            std::string password;
+
+            MSGPACK_DEFINE_MAP(session, id, password)
+        };
+        struct AnonRequest {
+            std::string session;
+
+            MSGPACK_DEFINE_MAP(session)
+        };
+        struct IdentifyResponse {
+            std::string session;
+            std::string id;
+            std::string sign;
+
+            MSGPACK_DEFINE_MAP(session, id, sign)
+        };
+
+        struct LeaveRequest {
+            std::string session;
+            std::string id;
+
+            MSGPACK_DEFINE_MAP(session, id)
+        };
+
+        struct LeaveResponse {
+            std::string session;
+            std::string id;
+
+            MSGPACK_DEFINE_MAP(session, id)
+        };
+
+        struct Identified {
+            std::string session, peer;
+            std::string id;
+            std::string sign;
+            std::shared_ptr<source<IdSessionMeta>> src;
+        };
+        struct Left {
+            std::string session, peer;
+            std::string id;
+        };
+
+        template<typename channelT, typename gateT, typename... MsgT>
+        class keeper : public cc::vortex<Identified, Left, MsgT...> {
+        private:
+            struct Session {
+                SessionCreated event;
+                std::shared_ptr<channelT> channel;
+                std::shared_ptr<wrapping_source<SessionMeta, IdSessionMeta>> source;
+            };
+        protected:
+            using sessionsT = std::unordered_map<std::string, Session>;
+
+            std::set<std::shared_ptr<gateT>> gate;
+
+            cc::aspect_ptr<const bool> dismissed = cc::forge<bool>(false);
+            cc::aspect_ptr<const sessionsT> sessions = cc::spawn<sessionsT>();
+
+            keeper(std::shared_ptr<cc::branch> b)
+            : cc::vortex<Identified, Left, MsgT...>(b) {};
+
+            virtual void connect(const std::shared_ptr<gateT>& g) = 0;
+
+            static void Dismiss(bool &dismissed) {
+                dismissed = true;
+            }
+
+        public:
+            virtual ~keeper() {
+                this->brnch->act(Dismiss, this->dismissed);
+            };
+
+            void attach(std::shared_ptr<gateT> g) {
+                const auto ret = this->gate.insert(g);
+
+                if(ret.second)
+                    this->connect(*ret.first);
+            }
+        };
+
+        template<typename channelT, typename gateT, typename spaceT>
+        class bouncer final : public keeper<channelT, gateT>, public std::enable_shared_from_this<bouncer<channelT, gateT, spaceT>> {
+        public:
+            using ptrT = std::shared_ptr<const bouncer<channelT, gateT, spaceT>>;
+            using keeperT = keeper<channelT, gateT>;
+        private:
+            using _ptrT = std::shared_ptr<bouncer<channelT, gateT, spaceT>>;
+
+            std::shared_ptr<spaceT> space;
+
+            static void Reg(const bool &dismissed, const std::string id, const std::string password, std::string &key) {
+                if(dismissed) {
+                    TRACE_WARNING(scope_data) << "Bouncer cannot register '" << id << "' because bouncer is dismissed";
+                    return;
+                }
+
+                // key is the hash from concatenation of id and password to avoid hash compare attacks
+                key = gnutls_derrive_key_sha(id+password, 256);
+                TRACE_INFO(scope_data) << "Bouncer succeffuly registered '" << id << "'";
+            }
+
+            static void Identify(ptrT b, const bool &dismissed, typename keeperT::sessionsT &sessions, const IdentifyRequest req, const SessionMeta meta, cc::aspect_ptr<std::string> &key) {
+                TRACE(scope_data) << "tick bouncer::Identify";
+                if(dismissed) {
+                    TRACE_WARNING(scope_data) << "Bouncer cannot identify because bouncer is dismissed";
+                    return;
+                }
+
+                const auto kkk = *key;
+                if(key && *key == gnutls_derrive_key_sha(req.id+req.password, 256)) {
+                    const IdSessionMeta id_meta{meta, {.id=req.id, .sign=req.id}};
+                    sessions[meta.session].source = wrapping_source<SessionMeta, IdSessionMeta>::make(
+                        sessions[meta.session].event.src,
+                        id_meta
+                    );
+                    b->trigger(Identified{.session=meta.session, .peer=meta.peer, .id=req.id, .sign=req.id, .src=std::dynamic_pointer_cast<source<IdSessionMeta>>(sessions[meta.session].source)});
+                    sessions[meta.session].channel->push(IdentifyResponse{.session = meta.session, .id = req.id, .sign = id_meta.sign});
+                    TRACE_INFO(scope_data) << "Bouncer succeffuly identified '" << req.id << "'";
+                } else {
+                    TRACE_INFO(scope_data) << "Bouncer cannot identify '" << req.id << "'";
+                }
+                TRACE(scope_data) << "ticked bouncer::Identify";
+            }
+
+            static void SourceOnIdentifyRequest(const IdentifyRequest e, const SessionMeta meta, ptrT b, const bool &dismissed) {
+                TRACE(scope_data) << "tick bouncer::SourceOnIdentifyRequest";
+                if(dismissed)
+                    TRACE_WARNING(scope_data) << "Bouncer cannot prepare to identify because bouncer is dismissed";
+                else
+                    cc::act_fwd(Identify, b, b->dismissed, b->sessions, e, meta, b->space->template get<std::string>(e.id));
+                TRACE(scope_data) << "ticked bouncer::SourceOnIdentifyRequest";
+            }
+
+            static void SourceOnLeaveRequest(const LeaveRequest e, const SessionMeta meta, ptrT b, const bool &dismissed, typename keeperT::sessionsT &sessions) {
+                TRACE(scope_data) << "tick bouncer::SourceOnLeaveRequest";
+                sessions[meta.session].source = nullptr;
+                b->trigger(Left{.session=meta.session, .peer=meta.peer, .id=e.id});
+                sessions[meta.session].channel->push(LeaveResponse{.session = meta.session, .id = e.id,});
+                TRACE(scope_data) << "ticked bouncer::SourceOnLeaveRequest";
+            }
+
+            static void GateSessionCreated(const SessionCreated e, _ptrT b, const bool &dismissed, typename keeperT::sessionsT &sessions) {
+                TRACE(scope_data) << "tick bouncer::GateSessionCreated";
+                if(dismissed) {
+                    TRACE_WARNING(scope_data) << "Bouncer cannot accept session because bouncer is dismissed";
+                    return;
+                }
+
+                sessions[e.session] = {.event=e, .channel=channelT::make(e.src, cc::action::current().context)};
+                sessions[e.session].channel->template subscribe<IdentifyRequest>(SourceOnIdentifyRequest, b, b->dismissed);
+                sessions[e.session].channel->template subscribe<LeaveRequest>(SourceOnLeaveRequest, b, b->dismissed, b->sessions);
+                TRACE(scope_data) << "ticked bouncer::GateSessionCreated";
+            }
+
+            static void GateOnSessionDisposed(const SessionDisposed s, ptrT b, typename keeperT::sessionsT &sessions) {
+                TRACE(scope_data) << "tick bouncer::GateOnSessionDisposed";
+                sessions.erase(s.session);
+                TRACE(scope_data) << "ticked bouncer::GateOnSessionDisposed";
+            }
+
+            bouncer(std::shared_ptr<cc::branch> b, std::shared_ptr<spaceT> s)
+            : keeper<channelT, gateT>(b), space(s) {
+                ASSERT_MSG(cc::scope_core, s != nullptr, "Bouncer requires a set space for storing and recalling registrations")
+            }
+
+        protected:
+            virtual void connect(const std::shared_ptr<gateT>& g) override {
+                g->template subscribe<SessionCreated>(GateSessionCreated, this->shared_from_this(), this->dismissed, this->sessions);
+                g->template subscribe<SessionDisposed>(GateOnSessionDisposed, this->shared_from_this(), this->sessions);
+            }
+
+        public:
+            static std::shared_ptr<bouncer<channelT, gateT, spaceT>> make(std::shared_ptr<cc::branch> b, std::shared_ptr<spaceT> s) {
+                return std::shared_ptr<bouncer<channelT, gateT, spaceT>>(new bouncer<channelT, gateT, spaceT>(b, s));
+            }
+
+            void reg(const std::string id, const std::string password) const {
+                this->brnch->act(Reg, this->dismissed, id, password, this->space->template take<std::string>(id));
+            }
+
+            void dereg(const std::string id) const {
+                this->space->dispose(id);
+                TRACE_INFO(scope_data) << "Bouncer deregistered '" << id << "'";
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/include/causal/data/memory.hpp b/include/causal/data/memory.hpp
index d2ee7e0d..38d07613 100644
--- a/include/causal/data/memory.hpp
+++ b/include/causal/data/memory.hpp
@@ -55,7 +55,7 @@ namespace causal {
         private:
             mutable std::recursive_mutex mtx;
 
-            std::unordered_map<ID, cc::weak_essence_ptr<>> aspects;
+            std::unordered_map<ID, cc::essence_ptr<>> aspects;
 
             template<typename GID> requires(std::is_same_v<ID, std::string>)
             static inline ID get_id(const GID& gid) {
@@ -73,9 +73,8 @@ namespace causal {
                 cc::aspect_ptr<T> r;
                 std::unique_lock l(this->mtx);
                 auto it = this->aspects.find(id);
-                if(it != this->aspects.end() && !it->second.expired()) {
-                    auto a = it->second.lock();
-                    r = a->template as_typed<std::remove_cv_t<T>>();
+                if(it != this->aspects.end()) {
+                    r = it->second->template as_typed<std::remove_cv_t<T>>();
                 }
                 
                 if(!r) {
@@ -83,6 +82,8 @@ namespace causal {
                         r = cc::essence<std::remove_cv_t<T>>::make_certain(id, this->shared_from_this(), std::forward<Args>(args)...);
                     else
                         r = cc::essence<std::remove_cv_t<T>>::make(id, this->shared_from_this(), std::forward<Args>(args)...);
+
+                    this->aspects.insert({id, r.get_ptr()->as_untyped()});
                 }
 
                 return r;
@@ -104,13 +105,14 @@ namespace causal {
                 }
             }
 
-            void destroy(const std::any id) noexcept override {
+            void dispose(const std::any id) noexcept override {
                 if(id.type() == typeid(ID)) {
                     std::unique_lock l(this->mtx);
                     auto it = this->aspects.find(std::any_cast<ID>(id));
-                    if(it->second.expired())
+                    if(it != this->aspects.end()) {
                         this->aspects.erase(it);
-                }                
+                    }
+                }
             }
 
             template<typename T, typename... Args>
@@ -140,9 +142,8 @@ namespace causal {
                 cc::aspect_ptr<T> r;
                 std::unique_lock l(this->mtx);
                 auto it = this->aspects.find(id);
-                if(it != this->aspects.end() && !it->second.expired()) {
-                    auto a = it->second.lock();
-                    r = a->template as_typed<std::remove_cv_t<T>>();
+                if(it != this->aspects.end()) {
+                    r = it->second->template as_typed<std::remove_cv_t<T>>();
                 }
 
                 return r;
diff --git a/test/data/channel.cpp b/test/data/channel.cpp
index 489aff94..f9cc6d35 100644
--- a/test/data/channel.cpp
+++ b/test/data/channel.cpp
@@ -57,33 +57,33 @@ TEST(causal_data_channel, msg_channel) {
     chk(2,3);
 }
 
-TEST(causal_data_channel, httpd_gate_manage_sess) {
+TEST(causal_data_channel, wrap) {
     struct Context {
         std::string session;
         std::shared_ptr<cd::source<cd::SessionMeta>> src;
         
-        static void Create(cd::SessionCreated e, Context &ctx) {
-            TRACE(cc::scope_core) << "tick Session_created '" << e.session << "'";
+        static void Create(const cd::SessionCreated e, Context &ctx) {
+            TRACE(cd::scope_data) << "tick Session_created '" << e.session << "'";
             ctx.session = e.session;
             ctx.src = e.src;
-            TRACE(cc::scope_core) << "ticked Session_created '" << e.session << "'";
+            TRACE(cd::scope_data) << "ticked Session_created '" << e.session << "'";
         }
-        static void Dispose(cd::SessionDisposed e, const Context &ctx) {
-            TRACE(cc::scope_core) << "tick Session_closed '" << e.session << "'";
+        static void Dispose(const cd::SessionDisposed e, const Context &ctx) {
+            TRACE(cd::scope_data) << "tick Session_closed '" << e.session << "'";
             EXPECT_EQ(ctx.session, e.session);
-            TRACE(cc::scope_core) << "ticked Session_closed '" << e.session << "'";
+            TRACE(cd::scope_data) << "ticked Session_closed '" << e.session << "'";
         };
     };
 
     const auto ctx = cc::spawn<Context>();
 
     auto d = cp::simple_dispatcher<>::make(); auto b = cc::branch::make(d);
-    const auto n = cd::wrap::make(b);
-    n->subscribe<cd::SessionCreated>(Context::Create, ctx);
-    n->subscribe<cd::SessionDisposed>(Context::Dispose, ctx);
+    const auto w = cd::wrap::make(b);
+    w->subscribe<cd::SessionCreated>(Context::Create, ctx);
+    w->subscribe<cd::SessionDisposed>(Context::Dispose, ctx);
 
     // initializing session
-    const auto s = n->forge();
+    const auto s = w->forge();
 
     EXPECT_TRUE(d->do_tick());
 
@@ -92,7 +92,9 @@ TEST(causal_data_channel, httpd_gate_manage_sess) {
         EXPECT_EQ(ctx->src, s.second);
     }
 
-    n->trigger(cd::DisposeSession{.session=s.first});
+    // TODO expand for triggering data as in keeper bouncer test
+
+    w->trigger(cd::DisposeSession{.session=s.first});
 
     EXPECT_TRUE(d->do_tick());
 }
diff --git a/test/data/keeper.cpp b/test/data/keeper.cpp
new file mode 100644
index 00000000..6478e6a1
--- /dev/null
+++ b/test/data/keeper.cpp
@@ -0,0 +1,104 @@
+// Licensed under the terms of the AGPL-3.0-only (GNU AFFERO GENERAL PUBLIC LICENSE version 3)
+// Copyright (C) 2018-2024 by Ralph Alexander Bariz
+
+#include <cstddef>
+#include <memory>
+
+#include <gtest/gtest.h>
+
+#include "causal/core/aspect.hpp"
+#include "causal/core/wirks.hpp"
+#include "causal/data/keeper.hpp"
+#include "causal/data/memory.hpp"
+#include "causal/data/msgpack.hpp"
+#include "causal/process/simple.hpp"
+#include "causal/process/thread.hpp"
+#include "causal/data/channel.hpp"
+#include "causal/trace.hpp"
+
+namespace cc = causal::core;
+namespace cp = causal::process;
+namespace cd = causal::data;
+
+TEST(causal_data_keeper, bouncer_login) {
+    using wrap_bouncer = cd::bouncer<cd::msgpack_channel<cd::SessionMeta>, cd::wrap, cd::memory_space<std::string>>;
+    struct Context {
+        std::string session;
+        std::string id;
+        std::string sign;
+        bool left = false;
+        std::shared_ptr<cd::source<cd::IdSessionMeta>> src;
+
+        static void OnIdentifyResponse(const cd::IdentifyResponse r, Context &ctx) {
+            TRACE(cd::scope_data) << "tick OnIdentifyResponse '" << r.id << "' with '" << r.sign << "' for '" << r.session << "'";
+            EXPECT_EQ(ctx.session, r.session);
+            ctx.id = r.id; ctx.sign = r.sign;
+            TRACE(cd::scope_data) << "ticked OnIdentifyResponse '" << r.session << "'";
+        }
+
+        static void OnLeaveResponse(const cd::LeaveResponse r, Context &ctx) {
+            TRACE(cd::scope_data) << "tick OnLeaveResponse '" << r.id << "' for '" << r.session << "'";
+            EXPECT_EQ(ctx.session, r.session);
+            EXPECT_EQ(ctx.id, r.id);
+            ctx.left = true;
+            TRACE(cd::scope_data) << "ticked OnLeaveResponse '" << r.session << "'";
+        }
+        
+        static void OnIdentified(const cd::Identified e, Context &ctx) {
+            TRACE(cd::scope_data) << "tick OnIdentified '" << e.id << "' with '" << e.sign << "' for '" << e.session << "' on '" << e.peer << "'";
+            ctx.session = e.session;
+            ctx.src = e.src;
+            TRACE(cd::scope_data) << "ticked OnIdentified '" << e.session << "'";
+        }
+        static void OnLeft(const cd::Left e, const Context &ctx) {
+            TRACE(cd::scope_data) << "tick OnLeft '" << e.id << "' from '" << e.session << "' on '" << e.peer << "'";
+            EXPECT_EQ(ctx.session, e.session);
+            TRACE(cd::scope_data) << "ticked OnLeft '" << e.session << "'";
+        };
+    };
+
+    const auto ctx = cc::spawn<Context>();
+    auto d = cp::simple_dispatcher<>::make(); auto b = cc::branch::make(d);
+    const auto w = cd::wrap::make(b);
+    const auto bspc = causal::data::memory_space<>::make();
+    const auto k = wrap_bouncer::make(b, bspc);
+    k->subscribe<cd::Identified>(Context::OnIdentified, ctx);
+    k->subscribe<cd::Left>(Context::OnLeft, ctx);
+    k->reg("testuser", "testpassword");
+
+    k->attach(w);
+
+    // initializing session
+    const auto s = w->forge();
+
+    while(d->do_tick()) {};
+
+    const auto c = cd::msgpack_channel<cd::SessionMeta>::make(s.second, b);
+    c->subscribe<cd::IdentifyResponse>(Context::OnIdentifyResponse, ctx);
+    c->subscribe<cd::LeaveResponse>(Context::OnLeaveResponse, ctx);
+
+    c->push(cd::IdentifyRequest{.session=s.first, .id="testuser", .password="testpassword"});
+
+    while(d->do_tick()) {};
+
+    {   cc::opener o(ctx);
+        EXPECT_EQ(ctx->id, "testuser");
+        EXPECT_FALSE(ctx->sign.empty());
+    }
+
+    c->push(cd::LeaveRequest{.session=s.first, .id="testuser"});
+
+    while(d->do_tick()) {};
+
+    {   cc::opener o(ctx);
+        EXPECT_TRUE(ctx->left);
+    }
+
+    k->dereg("testuser");
+
+    w->trigger(cd::DisposeSession{.session=s.first});
+
+    while(d->do_tick()) {};
+}
+
+int main(int argc, char** argv){testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();}
\ No newline at end of file
-- 
GitLab