treechain-adapters 1.0.0__tar.gz
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.
- treechain_adapters-1.0.0/LICENSE +12 -0
- treechain_adapters-1.0.0/PKG-INFO +120 -0
- treechain_adapters-1.0.0/README.md +76 -0
- treechain_adapters-1.0.0/pyproject.toml +65 -0
- treechain_adapters-1.0.0/setup.cfg +4 -0
- treechain_adapters-1.0.0/src/treechain_adapters/TEMPLATE.py +68 -0
- treechain_adapters-1.0.0/src/treechain_adapters/__init__.py +30 -0
- treechain_adapters-1.0.0/src/treechain_adapters/base.py +214 -0
- treechain_adapters-1.0.0/src/treechain_adapters/mongodb_adapter.py +134 -0
- treechain_adapters-1.0.0/src/treechain_adapters/mysql_adapter.py +105 -0
- treechain_adapters-1.0.0/src/treechain_adapters/postgresql_adapter.py +94 -0
- treechain_adapters-1.0.0/src/treechain_adapters/redis_adapter.py +91 -0
- treechain_adapters-1.0.0/src/treechain_adapters/sqlite_adapter.py +70 -0
- treechain_adapters-1.0.0/src/treechain_adapters/supabase_adapter.py +106 -0
- treechain_adapters-1.0.0/src/treechain_adapters.egg-info/PKG-INFO +120 -0
- treechain_adapters-1.0.0/src/treechain_adapters.egg-info/SOURCES.txt +18 -0
- treechain_adapters-1.0.0/src/treechain_adapters.egg-info/dependency_links.txt +1 -0
- treechain_adapters-1.0.0/src/treechain_adapters.egg-info/requires.txt +27 -0
- treechain_adapters-1.0.0/src/treechain_adapters.egg-info/top_level.txt +1 -0
- treechain_adapters-1.0.0/tests/test_adapters.py +182 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Proprietary License
|
|
2
|
+
Copyright (c) 2026 TreeChain Labs
|
|
3
|
+
Patent Pending — EP26025007.1
|
|
4
|
+
|
|
5
|
+
All rights reserved. This software and associated documentation
|
|
6
|
+
files are proprietary and confidential. Unauthorized copying,
|
|
7
|
+
modification, distribution, or use of this software, in whole
|
|
8
|
+
or in part, is strictly prohibited without explicit written
|
|
9
|
+
permission from TreeChain Labs.
|
|
10
|
+
|
|
11
|
+
For licensing inquiries: security@treechain.ai
|
|
12
|
+
For more information: treechain.ai
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: treechain-adapters
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: TreeChain database adapters — drop-in encryption for MongoDB, PostgreSQL, MySQL, Redis, SQLite, Supabase. Patent Pending — EP26025007.1
|
|
5
|
+
Author-email: TreeChain Labs <security@treechain.ai>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://treechain.ai
|
|
8
|
+
Project-URL: Documentation, https://api-eu.treechain.ai/docs
|
|
9
|
+
Keywords: encryption,database,mongodb,postgresql,mysql,redis,steganography,hipaa,zero-knowledge
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Security :: Cryptography
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: treechain>=4.0.0
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Provides-Extra: mongodb
|
|
27
|
+
Requires-Dist: motor>=3.0.0; extra == "mongodb"
|
|
28
|
+
Provides-Extra: postgresql
|
|
29
|
+
Requires-Dist: asyncpg>=0.27.0; extra == "postgresql"
|
|
30
|
+
Provides-Extra: mysql
|
|
31
|
+
Requires-Dist: aiomysql>=0.1.1; extra == "mysql"
|
|
32
|
+
Provides-Extra: sqlite
|
|
33
|
+
Requires-Dist: aiosqlite>=0.19.0; extra == "sqlite"
|
|
34
|
+
Provides-Extra: redis
|
|
35
|
+
Requires-Dist: redis>=5.0.0; extra == "redis"
|
|
36
|
+
Provides-Extra: supabase
|
|
37
|
+
Requires-Dist: supabase>=1.0.0; extra == "supabase"
|
|
38
|
+
Provides-Extra: all
|
|
39
|
+
Requires-Dist: treechain-adapters[mongodb,mysql,postgresql,redis,sqlite,supabase]; extra == "all"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# TreeChain Database Adapters
|
|
46
|
+
|
|
47
|
+
**Patent Pending — EP26025007.1**
|
|
48
|
+
|
|
49
|
+
Drop-in encryption for any database. Three lines of code.
|
|
50
|
+
Every field auto-encrypts with the Polyglottal Cipher.
|
|
51
|
+
Your database stays exactly as is — TreeChain wraps it transparently.
|
|
52
|
+
|
|
53
|
+
## Supported Databases
|
|
54
|
+
|
|
55
|
+
- **MongoDB** (via Motor async client)
|
|
56
|
+
- **PostgreSQL** (via asyncpg)
|
|
57
|
+
- **MySQL** (via aiomysql)
|
|
58
|
+
- **SQLite** (via aiosqlite)
|
|
59
|
+
- **Redis** (via redis-py async)
|
|
60
|
+
- **Supabase** (via supabase-py)
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install treechain-adapters
|
|
66
|
+
|
|
67
|
+
# With specific database support:
|
|
68
|
+
pip install treechain-adapters[mongodb]
|
|
69
|
+
pip install treechain-adapters[postgresql]
|
|
70
|
+
pip install treechain-adapters[all]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Before TreeChain:
|
|
77
|
+
client = AsyncIOMotorClient(MONGO_URI)
|
|
78
|
+
db = client.mydb
|
|
79
|
+
|
|
80
|
+
# After TreeChain (3 lines changed):
|
|
81
|
+
from treechain_adapters import MongoDBAdapter
|
|
82
|
+
|
|
83
|
+
tc = MongoDBAdapter(api_key="gj_live_...", customer_id="jordan")
|
|
84
|
+
db = tc.wrap(client.mydb)
|
|
85
|
+
|
|
86
|
+
# Everything else stays identical.
|
|
87
|
+
# Every write auto-encrypts via GlyphJammer.
|
|
88
|
+
# Every read auto-decrypts transparently.
|
|
89
|
+
# Portal populates in real time.
|
|
90
|
+
await db.patients.insert_one({"name": "John Smith", "ssn": "123-45-6789"})
|
|
91
|
+
# Stored as: {"name": "馏𜽡㌎𬵃...", "ssn": "𘫙𡢽甎똂..."}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## What Happens Under the Hood
|
|
95
|
+
|
|
96
|
+
1. You write data to your database as normal
|
|
97
|
+
2. The adapter intercepts string fields and encrypts them with GlyphJammer
|
|
98
|
+
3. Your database stores multilingual poetry instead of plaintext
|
|
99
|
+
4. On read, fields are transparently decrypted back
|
|
100
|
+
5. Your TreeChain portal shows all records in real time
|
|
101
|
+
6. DuckDB local copy created for redundancy
|
|
102
|
+
|
|
103
|
+
## Performance
|
|
104
|
+
|
|
105
|
+
- Zero blocking on writes — encryption runs in-line, portal population is fire-and-forget
|
|
106
|
+
- 68ms average encrypt latency on primary node
|
|
107
|
+
- Your application code does not change
|
|
108
|
+
|
|
109
|
+
## Links
|
|
110
|
+
|
|
111
|
+
- Website: [treechain.ai](https://treechain.ai)
|
|
112
|
+
- API Docs: [api-eu.treechain.ai/docs](https://api-eu.treechain.ai/docs)
|
|
113
|
+
- Support: security@treechain.ai
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
Proprietary — TreeChain Labs
|
|
118
|
+
Patent Pending — EP26025007.1
|
|
119
|
+
|
|
120
|
+
See [LICENSE](LICENSE) for full terms.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# TreeChain Database Adapters
|
|
2
|
+
|
|
3
|
+
**Patent Pending — EP26025007.1**
|
|
4
|
+
|
|
5
|
+
Drop-in encryption for any database. Three lines of code.
|
|
6
|
+
Every field auto-encrypts with the Polyglottal Cipher.
|
|
7
|
+
Your database stays exactly as is — TreeChain wraps it transparently.
|
|
8
|
+
|
|
9
|
+
## Supported Databases
|
|
10
|
+
|
|
11
|
+
- **MongoDB** (via Motor async client)
|
|
12
|
+
- **PostgreSQL** (via asyncpg)
|
|
13
|
+
- **MySQL** (via aiomysql)
|
|
14
|
+
- **SQLite** (via aiosqlite)
|
|
15
|
+
- **Redis** (via redis-py async)
|
|
16
|
+
- **Supabase** (via supabase-py)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install treechain-adapters
|
|
22
|
+
|
|
23
|
+
# With specific database support:
|
|
24
|
+
pip install treechain-adapters[mongodb]
|
|
25
|
+
pip install treechain-adapters[postgresql]
|
|
26
|
+
pip install treechain-adapters[all]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# Before TreeChain:
|
|
33
|
+
client = AsyncIOMotorClient(MONGO_URI)
|
|
34
|
+
db = client.mydb
|
|
35
|
+
|
|
36
|
+
# After TreeChain (3 lines changed):
|
|
37
|
+
from treechain_adapters import MongoDBAdapter
|
|
38
|
+
|
|
39
|
+
tc = MongoDBAdapter(api_key="gj_live_...", customer_id="jordan")
|
|
40
|
+
db = tc.wrap(client.mydb)
|
|
41
|
+
|
|
42
|
+
# Everything else stays identical.
|
|
43
|
+
# Every write auto-encrypts via GlyphJammer.
|
|
44
|
+
# Every read auto-decrypts transparently.
|
|
45
|
+
# Portal populates in real time.
|
|
46
|
+
await db.patients.insert_one({"name": "John Smith", "ssn": "123-45-6789"})
|
|
47
|
+
# Stored as: {"name": "馏𜽡㌎𬵃...", "ssn": "𘫙𡢽甎똂..."}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What Happens Under the Hood
|
|
51
|
+
|
|
52
|
+
1. You write data to your database as normal
|
|
53
|
+
2. The adapter intercepts string fields and encrypts them with GlyphJammer
|
|
54
|
+
3. Your database stores multilingual poetry instead of plaintext
|
|
55
|
+
4. On read, fields are transparently decrypted back
|
|
56
|
+
5. Your TreeChain portal shows all records in real time
|
|
57
|
+
6. DuckDB local copy created for redundancy
|
|
58
|
+
|
|
59
|
+
## Performance
|
|
60
|
+
|
|
61
|
+
- Zero blocking on writes — encryption runs in-line, portal population is fire-and-forget
|
|
62
|
+
- 68ms average encrypt latency on primary node
|
|
63
|
+
- Your application code does not change
|
|
64
|
+
|
|
65
|
+
## Links
|
|
66
|
+
|
|
67
|
+
- Website: [treechain.ai](https://treechain.ai)
|
|
68
|
+
- API Docs: [api-eu.treechain.ai/docs](https://api-eu.treechain.ai/docs)
|
|
69
|
+
- Support: security@treechain.ai
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
Proprietary — TreeChain Labs
|
|
74
|
+
Patent Pending — EP26025007.1
|
|
75
|
+
|
|
76
|
+
See [LICENSE](LICENSE) for full terms.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "treechain-adapters"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "TreeChain database adapters — drop-in encryption for MongoDB, PostgreSQL, MySQL, Redis, SQLite, Supabase. Patent Pending — EP26025007.1"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Proprietary"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "TreeChain Labs", email = "security@treechain.ai"}
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"encryption",
|
|
16
|
+
"database",
|
|
17
|
+
"mongodb",
|
|
18
|
+
"postgresql",
|
|
19
|
+
"mysql",
|
|
20
|
+
"redis",
|
|
21
|
+
"steganography",
|
|
22
|
+
"hipaa",
|
|
23
|
+
"zero-knowledge",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: Other/Proprietary License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.9",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Topic :: Database",
|
|
36
|
+
"Topic :: Security :: Cryptography",
|
|
37
|
+
]
|
|
38
|
+
requires-python = ">=3.9"
|
|
39
|
+
dependencies = [
|
|
40
|
+
"treechain>=4.0.0",
|
|
41
|
+
"httpx>=0.24.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
mongodb = ["motor>=3.0.0"]
|
|
46
|
+
postgresql = ["asyncpg>=0.27.0"]
|
|
47
|
+
mysql = ["aiomysql>=0.1.1"]
|
|
48
|
+
sqlite = ["aiosqlite>=0.19.0"]
|
|
49
|
+
redis = ["redis>=5.0.0"]
|
|
50
|
+
supabase = ["supabase>=1.0.0"]
|
|
51
|
+
all = [
|
|
52
|
+
"treechain-adapters[mongodb,postgresql,mysql,sqlite,redis,supabase]",
|
|
53
|
+
]
|
|
54
|
+
dev = [
|
|
55
|
+
"pytest>=7.0.0",
|
|
56
|
+
"pytest-asyncio>=0.21.0",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://treechain.ai"
|
|
61
|
+
Documentation = "https://api-eu.treechain.ai/docs"
|
|
62
|
+
|
|
63
|
+
[tool.setuptools.packages.find]
|
|
64
|
+
where = ["src"]
|
|
65
|
+
include = ["treechain_adapters*"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TreeChain [DATABASE_NAME] Adapter — TEMPLATE
|
|
3
|
+
|
|
4
|
+
Copy this file and implement 3 things:
|
|
5
|
+
1. Wrap the client class to intercept writes
|
|
6
|
+
2. Call self._adapter._auto_populate() on every write
|
|
7
|
+
3. Increment self._adapter._write_count / _read_count
|
|
8
|
+
|
|
9
|
+
Time to implement: ~20 minutes for a new database.
|
|
10
|
+
|
|
11
|
+
Steps:
|
|
12
|
+
1. Copy this file as yourdb_adapter.py
|
|
13
|
+
2. Replace _WrappedClient with your DB's client wrapper
|
|
14
|
+
3. Intercept the write methods (insert, execute, set, etc.)
|
|
15
|
+
4. Call _auto_populate with the data and a source name
|
|
16
|
+
5. Add to __init__.py exports
|
|
17
|
+
6. Test with: python -m pytest sdk/tests/test_adapters.py
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict
|
|
21
|
+
from .base import TreeChainDBAdapter
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _WrappedClient:
|
|
25
|
+
"""Wrap the database client. Intercept writes, pass through reads."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client, adapter: 'YourDBAdapter'):
|
|
28
|
+
self._client = client
|
|
29
|
+
self._adapter = adapter
|
|
30
|
+
|
|
31
|
+
async def your_write_method(self, *args, **kwargs):
|
|
32
|
+
"""Replace with the actual write method name (insert, execute, set, etc.)"""
|
|
33
|
+
# 1. Call the original method first — never block the customer
|
|
34
|
+
result = await self._client.your_write_method(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
# 2. Increment write counter
|
|
37
|
+
self._adapter._write_count += 1
|
|
38
|
+
|
|
39
|
+
# 3. Auto-populate portal (fire-and-forget, never throws)
|
|
40
|
+
await self._adapter._auto_populate(
|
|
41
|
+
{"data": str(args)[:500]}, # Extract meaningful fields from args
|
|
42
|
+
"your_table_writes", # Collection name in portal
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 4. Return original result — customer sees no difference
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
async def your_read_method(self, *args, **kwargs):
|
|
49
|
+
"""Reads just pass through with a counter increment."""
|
|
50
|
+
self._adapter._read_count += 1
|
|
51
|
+
return await self._client.your_read_method(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
def __getattr__(self, name):
|
|
54
|
+
"""Pass through anything we don't intercept."""
|
|
55
|
+
return getattr(self._client, name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class YourDBAdapter(TreeChainDBAdapter):
|
|
59
|
+
"""
|
|
60
|
+
[DATABASE_NAME] adapter for TreeChain.
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
tc = YourDBAdapter(api_key="gj_live_...", customer_id="jordan")
|
|
64
|
+
client = tc.wrap(your_db_client)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def wrap(self, client) -> _WrappedClient:
|
|
68
|
+
return _WrappedClient(client, self)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TreeChain Database Adapters
|
|
3
|
+
|
|
4
|
+
Drop-in wrappers for every major database. 3 lines to integrate:
|
|
5
|
+
|
|
6
|
+
from treechain.db_adapters import MongoDBAdapter
|
|
7
|
+
tc = MongoDBAdapter(api_key="gj_live_...", customer_id="jordan")
|
|
8
|
+
db = tc.wrap(your_existing_db_client)
|
|
9
|
+
|
|
10
|
+
Every write auto-encrypts and populates your TreeChain portal.
|
|
11
|
+
Zero latency impact. Zero code changes beyond the 3 lines above.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .base import TreeChainDBAdapter
|
|
15
|
+
from .mongodb_adapter import MongoDBAdapter
|
|
16
|
+
from .postgresql_adapter import PostgreSQLAdapter
|
|
17
|
+
from .mysql_adapter import MySQLAdapter
|
|
18
|
+
from .sqlite_adapter import SQLiteAdapter
|
|
19
|
+
from .redis_adapter import RedisAdapter
|
|
20
|
+
from .supabase_adapter import SupabaseAdapter
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"TreeChainDBAdapter",
|
|
24
|
+
"MongoDBAdapter",
|
|
25
|
+
"PostgreSQLAdapter",
|
|
26
|
+
"MySQLAdapter",
|
|
27
|
+
"SQLiteAdapter",
|
|
28
|
+
"RedisAdapter",
|
|
29
|
+
"SupabaseAdapter",
|
|
30
|
+
]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TreeChain DB Adapter — Base Class
|
|
3
|
+
|
|
4
|
+
Every database adapter inherits from this. Intercept writes,
|
|
5
|
+
encrypt fields via GlyphJammer, auto-populate the customer portal
|
|
6
|
+
in real time. Zero latency impact. Zero code change for the customer.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from treechain.db_adapters import MongoDBAdapter
|
|
10
|
+
tc = MongoDBAdapter(api_key="gj_live_...", customer_id="jordan")
|
|
11
|
+
db = tc.wrap(client.mydb)
|
|
12
|
+
# Everything else stays identical — every write auto-populates
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger("treechain.db_adapter")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TreeChainDBAdapter(ABC):
|
|
30
|
+
"""
|
|
31
|
+
Base class for all TreeChain database adapters.
|
|
32
|
+
|
|
33
|
+
Drop-in wrapper: intercepts reads/writes, encrypts field values
|
|
34
|
+
via GlyphJammer, and fires async hooks to populate the customer's
|
|
35
|
+
encrypted database portal. The main database operation is never
|
|
36
|
+
blocked or slowed.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: str = None,
|
|
42
|
+
customer_id: str = None,
|
|
43
|
+
collection_prefix: str = "cdb",
|
|
44
|
+
mongo_uri: str = None,
|
|
45
|
+
master_key: str = None,
|
|
46
|
+
auto_populate: bool = True,
|
|
47
|
+
encrypt_fields: bool = True,
|
|
48
|
+
):
|
|
49
|
+
self.api_key = api_key
|
|
50
|
+
self.customer_id = customer_id or self._resolve_customer_id(api_key)
|
|
51
|
+
self.collection_prefix = collection_prefix
|
|
52
|
+
self.auto_populate = auto_populate
|
|
53
|
+
self.encrypt_fields = encrypt_fields
|
|
54
|
+
self._mongo_uri = mongo_uri or os.getenv("MONGODB_URI") or os.getenv("MONGO_URI")
|
|
55
|
+
self._master_key = master_key or os.getenv("TREECHAIN_MASTER_KEY")
|
|
56
|
+
self._jammer = None
|
|
57
|
+
self._mongo_client = None
|
|
58
|
+
self._mongo_db = None
|
|
59
|
+
self._duckdb = None
|
|
60
|
+
self._write_count = 0
|
|
61
|
+
self._read_count = 0
|
|
62
|
+
self._populate_count = 0
|
|
63
|
+
self._error_count = 0
|
|
64
|
+
|
|
65
|
+
def _resolve_customer_id(self, api_key: str) -> str:
|
|
66
|
+
"""Derive customer_id from API key hash if not provided."""
|
|
67
|
+
if not api_key:
|
|
68
|
+
return "anonymous"
|
|
69
|
+
raw = api_key.strip()
|
|
70
|
+
if raw.startswith("tc_"):
|
|
71
|
+
raw = raw[3:]
|
|
72
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:12]
|
|
73
|
+
|
|
74
|
+
def _get_jammer(self):
|
|
75
|
+
"""Lazy-init GlyphJammer."""
|
|
76
|
+
if self._jammer is None and self._master_key:
|
|
77
|
+
try:
|
|
78
|
+
import sys
|
|
79
|
+
for p in [
|
|
80
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "vendor", "treechain_sdk", "treechain-sdk-v4"),
|
|
81
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "treechain-sdk-v4"),
|
|
82
|
+
]:
|
|
83
|
+
if os.path.exists(p) and p not in sys.path:
|
|
84
|
+
sys.path.insert(0, p)
|
|
85
|
+
from treechain.core.encryption import GlyphJammer
|
|
86
|
+
self._jammer = GlyphJammer(master_key=self._master_key)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
log.warning("GlyphJammer init failed: %s", e)
|
|
89
|
+
return self._jammer
|
|
90
|
+
|
|
91
|
+
def _get_mongo(self):
|
|
92
|
+
"""Lazy-init MongoDB client for portal writes."""
|
|
93
|
+
if self._mongo_client is None and self._mongo_uri:
|
|
94
|
+
try:
|
|
95
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
96
|
+
self._mongo_client = AsyncIOMotorClient(
|
|
97
|
+
self._mongo_uri, maxPoolSize=5, minPoolSize=1,
|
|
98
|
+
serverSelectionTimeoutMS=5000,
|
|
99
|
+
)
|
|
100
|
+
self._mongo_db = self._mongo_client[os.getenv("MONGO_DB", "glyphjammer")]
|
|
101
|
+
except Exception as e:
|
|
102
|
+
log.warning("MongoDB portal connection failed: %s", e)
|
|
103
|
+
return self._mongo_db
|
|
104
|
+
|
|
105
|
+
def _get_duckdb(self):
|
|
106
|
+
"""Lazy-init DuckDB for local redundancy."""
|
|
107
|
+
if self._duckdb is None:
|
|
108
|
+
try:
|
|
109
|
+
from stores.duckdb_store import DuckDBProvenanceStore
|
|
110
|
+
self._duckdb = DuckDBProvenanceStore(
|
|
111
|
+
master_key=self._master_key,
|
|
112
|
+
node_id=os.getenv("NODE_ID", "unknown"),
|
|
113
|
+
)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
return self._duckdb
|
|
117
|
+
|
|
118
|
+
def _ns(self, source: str) -> str:
|
|
119
|
+
"""Build namespaced collection name."""
|
|
120
|
+
safe_cust = "".join(c for c in self.customer_id if c.isalnum() or c in ("_", "-"))[:64]
|
|
121
|
+
safe_src = "".join(c for c in source if c.isalnum() or c in ("_", "-"))[:64]
|
|
122
|
+
return f"{self.collection_prefix}_{safe_cust}_{safe_src}"
|
|
123
|
+
|
|
124
|
+
def _encrypt_doc(self, data: Dict) -> tuple:
|
|
125
|
+
"""Encrypt all string values in a document. Returns (encrypted_doc, encrypted_fields_meta)."""
|
|
126
|
+
jammer = self._get_jammer()
|
|
127
|
+
if not jammer or not self.encrypt_fields:
|
|
128
|
+
return data, {}
|
|
129
|
+
|
|
130
|
+
doc = {}
|
|
131
|
+
ef = {}
|
|
132
|
+
for k, v in data.items():
|
|
133
|
+
if k.startswith("_"):
|
|
134
|
+
doc[k] = v
|
|
135
|
+
continue
|
|
136
|
+
if isinstance(v, str) and v:
|
|
137
|
+
try:
|
|
138
|
+
result = jammer.encrypt(str(v), emotion="love")
|
|
139
|
+
doc[k] = getattr(result, "glyph_payload", str(result))
|
|
140
|
+
ef[k] = {
|
|
141
|
+
"shield_id": getattr(result, "shield_id", ""),
|
|
142
|
+
"meta": getattr(result, "metadata", {}),
|
|
143
|
+
}
|
|
144
|
+
except Exception:
|
|
145
|
+
doc[k] = v
|
|
146
|
+
else:
|
|
147
|
+
doc[k] = v
|
|
148
|
+
return doc, ef
|
|
149
|
+
|
|
150
|
+
async def _auto_populate(self, data: Dict, source: str, operation: str = "write"):
|
|
151
|
+
"""
|
|
152
|
+
Fire-and-forget portal population.
|
|
153
|
+
Never blocks. Never throws. Never slows the customer's operation.
|
|
154
|
+
"""
|
|
155
|
+
if not self.auto_populate:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
enc_doc, ef = self._encrypt_doc(data)
|
|
160
|
+
enc_doc["_encrypted_fields"] = ef
|
|
161
|
+
enc_doc["_created_at"] = datetime.now(timezone.utc)
|
|
162
|
+
enc_doc["_customer_id"] = self.customer_id
|
|
163
|
+
enc_doc["_source_adapter"] = self.__class__.__name__
|
|
164
|
+
enc_doc["_operation"] = operation
|
|
165
|
+
|
|
166
|
+
ns = self._ns(source)
|
|
167
|
+
|
|
168
|
+
# MongoDB async write
|
|
169
|
+
db = self._get_mongo()
|
|
170
|
+
if db is not None:
|
|
171
|
+
asyncio.ensure_future(db[ns].insert_one(enc_doc))
|
|
172
|
+
|
|
173
|
+
# DuckDB local write
|
|
174
|
+
ddb = self._get_duckdb()
|
|
175
|
+
if ddb:
|
|
176
|
+
ddb.store_async(ns, json.dumps(ef, default=str)[:200], enc_doc)
|
|
177
|
+
|
|
178
|
+
self._populate_count += 1
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self._error_count += 1
|
|
181
|
+
log.debug("Auto-populate failed (non-fatal): %s", e)
|
|
182
|
+
|
|
183
|
+
def _auto_populate_sync(self, data: Dict, source: str, operation: str = "write"):
|
|
184
|
+
"""Sync wrapper for auto_populate — used by sync adapters."""
|
|
185
|
+
try:
|
|
186
|
+
loop = asyncio.get_event_loop()
|
|
187
|
+
if loop.is_running():
|
|
188
|
+
asyncio.ensure_future(self._auto_populate(data, source, operation))
|
|
189
|
+
else:
|
|
190
|
+
loop.run_until_complete(self._auto_populate(data, source, operation))
|
|
191
|
+
except RuntimeError:
|
|
192
|
+
# No event loop — fire in background thread
|
|
193
|
+
threading.Thread(
|
|
194
|
+
target=lambda: asyncio.run(self._auto_populate(data, source, operation)),
|
|
195
|
+
daemon=True,
|
|
196
|
+
).start()
|
|
197
|
+
|
|
198
|
+
def stats(self) -> Dict:
|
|
199
|
+
"""Adapter statistics."""
|
|
200
|
+
return {
|
|
201
|
+
"adapter": self.__class__.__name__,
|
|
202
|
+
"customer_id": self.customer_id,
|
|
203
|
+
"writes": self._write_count,
|
|
204
|
+
"reads": self._read_count,
|
|
205
|
+
"populated": self._populate_count,
|
|
206
|
+
"errors": self._error_count,
|
|
207
|
+
"auto_populate": self.auto_populate,
|
|
208
|
+
"encrypt_fields": self.encrypt_fields,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def wrap(self, client: Any) -> Any:
|
|
213
|
+
"""Wrap the customer's database client. Returns the wrapped client."""
|
|
214
|
+
...
|