nedb-engine 2.2.28__tar.gz → 2.2.32__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.
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/PKG-INFO +13 -12
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/README.md +12 -11
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/pyproject.toml +1 -1
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/__init__.py +29 -13
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/_native.abi3.so +0 -0
- nedb_engine-2.2.32/python/nedb/nedbd-v2 +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/nedbd-v2-darwin-arm64 +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/nedbd-v2-darwin-x64 +0 -0
- nedb_engine-2.2.32/python/nedb/nedbd-v2.exe +0 -0
- nedb_engine-2.2.32/python/nedb/proof.py +126 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/server.py +78 -2
- nedb_engine-2.2.32/rust/nedb-v2/README.md +179 -0
- nedb_engine-2.2.28/python/nedb/nedbd-v2 +0 -0
- nedb_engine-2.2.28/python/nedb/nedbd-v2.exe +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/.gitignore +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/LICENSE +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/client/node/README.md +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/client/python/README.md +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/autoindex.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/backends/__init__.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/backends/redis_backend.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/cascade.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/concurrent.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/crypto.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/engine.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/index.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/log.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/merkle.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/mongo.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/query.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/redis_compat.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/relations.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/resp2.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/snapshot.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/sql.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/store.py +0 -0
- {nedb_engine-2.2.28 → nedb_engine-2.2.32}/python/nedb/wrap_redis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedb-engine
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.32
|
|
4
4
|
Summary: NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd).
|
|
5
5
|
Project-URL: Homepage, https://github.com/aiassistsecure/nedb
|
|
6
6
|
Project-URL: Repository, https://github.com/aiassistsecure/nedb
|
|
@@ -29,8 +29,9 @@ Replay-protected · idempotent · relational · filterable · sortable · search
|
|
|
29
29
|
One Rust core → ships to **PyPI** and **npm** from a single source.
|
|
30
30
|
|
|
31
31
|
[](https://pypi.org/project/nedb-engine/)
|
|
32
|
+
[](https://crates.io/crates/nedb-engine)
|
|
32
33
|
[](https://www.npmjs.com/package/nedb-engine)
|
|
33
|
-
[](https://github.com/Eth-Interchained/nedb/actions)
|
|
34
35
|
[](https://pypi.org/project/nedb-engine-client/)
|
|
35
36
|
[](https://www.npmjs.com/package/nedb-engine-client)
|
|
36
37
|
|
|
@@ -40,11 +41,11 @@ One Rust core → ships to **PyPI** and **npm** from a single source.
|
|
|
40
41
|
|
|
41
42
|
---
|
|
42
43
|
|
|
43
|
-
## NEDB v2.
|
|
44
|
+
## NEDB v2.2.31 — Production Stable
|
|
44
45
|
|
|
45
|
-
**Current stable: 2.
|
|
46
|
+
**Current stable: 2.2.31** — Cross-platform native wheels shipping `nedbd-v2` binary inside `pip install nedb-engine`. Linux + Windows wheels built on GitHub Actions; macOS arm64 + x86_64 wheels built on Codemagic (M2 Mac Minis). All four platforms publish from one `v*` tag.
|
|
46
47
|
|
|
47
|
-
NEDB v2 replaces the append-only log (AOF) with a **content-addressed Merkle DAG**. Every document version is an immutable, BLAKE2b-verified object. Nothing is ever overwritten. As of **v2.
|
|
48
|
+
NEDB v2 replaces the append-only log (AOF) with a **content-addressed Merkle DAG**. Every document version is an immutable, BLAKE2b-verified object. Nothing is ever overwritten. As of **v2.2.31**, restarts after the first open are **O(1) warm starts** (driven by a `MANIFEST` of `seq` + Merkle head), the **cold scan is deferred** so the daemon accepts connections immediately, and a new **`GET /events` SSE endpoint** streams scan progress + per-write events live.
|
|
48
49
|
|
|
49
50
|
```bash
|
|
50
51
|
# Run the v2 DAG engine — ships inside pip install nedb-engine
|
|
@@ -53,9 +54,9 @@ nedbd --dag --data ./data
|
|
|
53
54
|
NEDBD_DAG=1 NEDB_TMK=<32-byte-hex> nedbd --data ./data
|
|
54
55
|
|
|
55
56
|
curl http://127.0.0.1:7070/health
|
|
56
|
-
# {"ok":true,"version":"2.
|
|
57
|
+
# {"ok":true,"version":"2.2.31","service":"nedbd","engine":"dag","startup_ready":true,"encrypted":true}
|
|
57
58
|
|
|
58
|
-
# Tail the live event stream (new in v2.
|
|
59
|
+
# Tail the live event stream (new in v2.2.31)
|
|
59
60
|
curl http://127.0.0.1:7070/events
|
|
60
61
|
# event: scan data: {"objects":730000,"of":1310703,"rate":21043,"eta_s":28}
|
|
61
62
|
# event: ready data: {"seq":1310703,"head":"b2:9c14e07a…"}
|
|
@@ -79,7 +80,7 @@ curl http://127.0.0.1:7070/events
|
|
|
79
80
|
|
|
80
81
|
**v1 AOF engine is still shipped and unchanged** — `nedbd` (no flag) runs v1.
|
|
81
82
|
|
|
82
|
-
**Production status:** [vision.interchained.org](https://vision.interchained.org) is live on v2.
|
|
83
|
+
**Production status:** [vision.interchained.org](https://vision.interchained.org) is live on v2.2.31 — **1,310,703 sequences** indexed in the Vision database, AES-256-GCM encrypted at rest, at block height **620,989**.
|
|
83
84
|
|
|
84
85
|
---
|
|
85
86
|
|
|
@@ -244,11 +245,11 @@ nedbd --dag --data ./data # v2 DAG engine (or NEDBD_DAG=1)
|
|
|
244
245
|
NEDBD_RESP2_PORT=6380 nedbd # also speak RESP2 (redis-cli compatible)
|
|
245
246
|
nedbd --log-level 2 # 0=errors 1=requests 2=deploy 3=verbose
|
|
246
247
|
|
|
247
|
-
# Live event stream (new in v2.
|
|
248
|
+
# Live event stream (new in v2.2.31) — SSE: scan progress, ready, per-write head
|
|
248
249
|
curl http://127.0.0.1:7070/events
|
|
249
250
|
```
|
|
250
251
|
|
|
251
|
-
### Startup modes (v2.
|
|
252
|
+
### Startup modes (v2.2.31)
|
|
252
253
|
|
|
253
254
|
- **Warm start** — every restart after the first open reads the `MANIFEST` file and restores `seq` + Merkle `head` in **O(1)**. No scan, no replay, independent of dataset size. Boots in milliseconds.
|
|
254
255
|
- **Cold start** — first open of an existing dataset spawns the integrity scan in a background thread *and accepts connections immediately*. Reads serve instantly from the content-addressed DAG; writes return `HTTP 503 startup in progress` until the `startup_ready` gate flips. Progress (objects, rate, ETA) streams over `GET /events`.
|
|
@@ -258,7 +259,7 @@ curl http://127.0.0.1:7070/events
|
|
|
258
259
|
| Variable | Default | Description |
|
|
259
260
|
|---|---|---|
|
|
260
261
|
| `NEDBD_DAG` | `0` | Set `1` to launch the v2 DAG engine (`nedbd-v2`). Same as `--dag`. |
|
|
261
|
-
| `NEDBD_HOST` | `127.0.0.1` | Bind address. **v2.
|
|
262
|
+
| `NEDBD_HOST` | `127.0.0.1` | Bind address. **v2.2.31** defaults to loopback (was `0.0.0.0`) — security hardening fix. Set explicitly to `0.0.0.0` to expose. |
|
|
262
263
|
| `NEDBD_PORT` | `7070` | HTTP bind port. |
|
|
263
264
|
| `NEDBD_TOKEN` | unset | Optional bearer token; required on every `/v1/*` request when set. |
|
|
264
265
|
| `NEDB_TMK` | unset | 32-byte hex AES-256-GCM at-rest encryption key. |
|
|
@@ -321,7 +322,7 @@ db.query('FROM policy AS OF 200 VALID AS OF "2024-02-15"')
|
|
|
321
322
|
|
|
322
323
|
## Performance
|
|
323
324
|
|
|
324
|
-
**v2 DAG Rust server (v2.
|
|
325
|
+
**v2 DAG Rust server (v2.2.31, Intel iMac — 10k writes / 100k reads / 30k objects, AES-256-GCM on):**
|
|
325
326
|
|
|
326
327
|
| Operation | Throughput | p50 | p99 |
|
|
327
328
|
|---|---|---|---|
|
|
@@ -8,8 +8,9 @@ Replay-protected · idempotent · relational · filterable · sortable · search
|
|
|
8
8
|
One Rust core → ships to **PyPI** and **npm** from a single source.
|
|
9
9
|
|
|
10
10
|
[](https://pypi.org/project/nedb-engine/)
|
|
11
|
+
[](https://crates.io/crates/nedb-engine)
|
|
11
12
|
[](https://www.npmjs.com/package/nedb-engine)
|
|
12
|
-
[](https://github.com/Eth-Interchained/nedb/actions)
|
|
13
14
|
[](https://pypi.org/project/nedb-engine-client/)
|
|
14
15
|
[](https://www.npmjs.com/package/nedb-engine-client)
|
|
15
16
|
|
|
@@ -19,11 +20,11 @@ One Rust core → ships to **PyPI** and **npm** from a single source.
|
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
22
|
-
## NEDB v2.
|
|
23
|
+
## NEDB v2.2.31 — Production Stable
|
|
23
24
|
|
|
24
|
-
**Current stable: 2.
|
|
25
|
+
**Current stable: 2.2.31** — Cross-platform native wheels shipping `nedbd-v2` binary inside `pip install nedb-engine`. Linux + Windows wheels built on GitHub Actions; macOS arm64 + x86_64 wheels built on Codemagic (M2 Mac Minis). All four platforms publish from one `v*` tag.
|
|
25
26
|
|
|
26
|
-
NEDB v2 replaces the append-only log (AOF) with a **content-addressed Merkle DAG**. Every document version is an immutable, BLAKE2b-verified object. Nothing is ever overwritten. As of **v2.
|
|
27
|
+
NEDB v2 replaces the append-only log (AOF) with a **content-addressed Merkle DAG**. Every document version is an immutable, BLAKE2b-verified object. Nothing is ever overwritten. As of **v2.2.31**, restarts after the first open are **O(1) warm starts** (driven by a `MANIFEST` of `seq` + Merkle head), the **cold scan is deferred** so the daemon accepts connections immediately, and a new **`GET /events` SSE endpoint** streams scan progress + per-write events live.
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
30
|
# Run the v2 DAG engine — ships inside pip install nedb-engine
|
|
@@ -32,9 +33,9 @@ nedbd --dag --data ./data
|
|
|
32
33
|
NEDBD_DAG=1 NEDB_TMK=<32-byte-hex> nedbd --data ./data
|
|
33
34
|
|
|
34
35
|
curl http://127.0.0.1:7070/health
|
|
35
|
-
# {"ok":true,"version":"2.
|
|
36
|
+
# {"ok":true,"version":"2.2.31","service":"nedbd","engine":"dag","startup_ready":true,"encrypted":true}
|
|
36
37
|
|
|
37
|
-
# Tail the live event stream (new in v2.
|
|
38
|
+
# Tail the live event stream (new in v2.2.31)
|
|
38
39
|
curl http://127.0.0.1:7070/events
|
|
39
40
|
# event: scan data: {"objects":730000,"of":1310703,"rate":21043,"eta_s":28}
|
|
40
41
|
# event: ready data: {"seq":1310703,"head":"b2:9c14e07a…"}
|
|
@@ -58,7 +59,7 @@ curl http://127.0.0.1:7070/events
|
|
|
58
59
|
|
|
59
60
|
**v1 AOF engine is still shipped and unchanged** — `nedbd` (no flag) runs v1.
|
|
60
61
|
|
|
61
|
-
**Production status:** [vision.interchained.org](https://vision.interchained.org) is live on v2.
|
|
62
|
+
**Production status:** [vision.interchained.org](https://vision.interchained.org) is live on v2.2.31 — **1,310,703 sequences** indexed in the Vision database, AES-256-GCM encrypted at rest, at block height **620,989**.
|
|
62
63
|
|
|
63
64
|
---
|
|
64
65
|
|
|
@@ -223,11 +224,11 @@ nedbd --dag --data ./data # v2 DAG engine (or NEDBD_DAG=1)
|
|
|
223
224
|
NEDBD_RESP2_PORT=6380 nedbd # also speak RESP2 (redis-cli compatible)
|
|
224
225
|
nedbd --log-level 2 # 0=errors 1=requests 2=deploy 3=verbose
|
|
225
226
|
|
|
226
|
-
# Live event stream (new in v2.
|
|
227
|
+
# Live event stream (new in v2.2.31) — SSE: scan progress, ready, per-write head
|
|
227
228
|
curl http://127.0.0.1:7070/events
|
|
228
229
|
```
|
|
229
230
|
|
|
230
|
-
### Startup modes (v2.
|
|
231
|
+
### Startup modes (v2.2.31)
|
|
231
232
|
|
|
232
233
|
- **Warm start** — every restart after the first open reads the `MANIFEST` file and restores `seq` + Merkle `head` in **O(1)**. No scan, no replay, independent of dataset size. Boots in milliseconds.
|
|
233
234
|
- **Cold start** — first open of an existing dataset spawns the integrity scan in a background thread *and accepts connections immediately*. Reads serve instantly from the content-addressed DAG; writes return `HTTP 503 startup in progress` until the `startup_ready` gate flips. Progress (objects, rate, ETA) streams over `GET /events`.
|
|
@@ -237,7 +238,7 @@ curl http://127.0.0.1:7070/events
|
|
|
237
238
|
| Variable | Default | Description |
|
|
238
239
|
|---|---|---|
|
|
239
240
|
| `NEDBD_DAG` | `0` | Set `1` to launch the v2 DAG engine (`nedbd-v2`). Same as `--dag`. |
|
|
240
|
-
| `NEDBD_HOST` | `127.0.0.1` | Bind address. **v2.
|
|
241
|
+
| `NEDBD_HOST` | `127.0.0.1` | Bind address. **v2.2.31** defaults to loopback (was `0.0.0.0`) — security hardening fix. Set explicitly to `0.0.0.0` to expose. |
|
|
241
242
|
| `NEDBD_PORT` | `7070` | HTTP bind port. |
|
|
242
243
|
| `NEDBD_TOKEN` | unset | Optional bearer token; required on every `/v1/*` request when set. |
|
|
243
244
|
| `NEDB_TMK` | unset | 32-byte hex AES-256-GCM at-rest encryption key. |
|
|
@@ -300,7 +301,7 @@ db.query('FROM policy AS OF 200 VALID AS OF "2024-02-15"')
|
|
|
300
301
|
|
|
301
302
|
## Performance
|
|
302
303
|
|
|
303
|
-
**v2 DAG Rust server (v2.
|
|
304
|
+
**v2 DAG Rust server (v2.2.31, Intel iMac — 10k writes / 100k reads / 30k objects, AES-256-GCM on):**
|
|
304
305
|
|
|
305
306
|
| Operation | Throughput | p50 | p99 |
|
|
306
307
|
|---|---|---|---|
|
|
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "nedb-engine"
|
|
13
|
-
version = "2.2.
|
|
13
|
+
version = "2.2.32"
|
|
14
14
|
description = "NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd)."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.8"
|
|
@@ -29,6 +29,7 @@ from .mongo import (
|
|
|
29
29
|
from .autoindex import AutoIndexDB
|
|
30
30
|
from .concurrent import Sequencer
|
|
31
31
|
from .wrap_redis import wrap_redis, WrappedRedis
|
|
32
|
+
from .proof import verify_proof, fold_head
|
|
32
33
|
|
|
33
34
|
try: # compiled Rust core, present in platform wheels (PyO3 via maturin)
|
|
34
35
|
from . import _native # type: ignore
|
|
@@ -38,21 +39,35 @@ except ImportError: # pure-Python install (sdist / unsupported platform)
|
|
|
38
39
|
# informative error instead of a bare ImportError with no guidance.
|
|
39
40
|
import types as _types, sys as _sys
|
|
40
41
|
|
|
42
|
+
import sys as _sys_tmp, os as _os_tmp
|
|
43
|
+
_is_msys2 = bool(_os_tmp.environ.get("MSYSTEM")) or "mingw" in _sys_tmp.executable.lower()
|
|
44
|
+
del _sys_tmp, _os_tmp
|
|
45
|
+
|
|
41
46
|
class _NativeStub(_types.ModuleType):
|
|
42
|
-
|
|
47
|
+
# Primary fix: install the Rust crate → get the nedbd server → use HTTP mode.
|
|
48
|
+
# Secondary fix (CPython only): pip reinstall to get the platform wheel with _native embedded.
|
|
49
|
+
_MSG_MSYS2 = (
|
|
50
|
+
"\n\n"
|
|
51
|
+
" nedb._native (embedded v2 DAG core) is not available on MSYS2/MinGW Python.\n\n"
|
|
52
|
+
" To use NEDB v2 features, install the server binary and use HTTP mode:\n\n"
|
|
53
|
+
" cargo install nedb-engine # install nedbd v2 server\n"
|
|
54
|
+
" nedbd --dag ./data # start DAG server\n"
|
|
55
|
+
" NEDB_URL=http://localhost:7070 python3 your_script.py\n\n"
|
|
56
|
+
" Run 'nedbd --doctor' for a full diagnosis.\n"
|
|
57
|
+
)
|
|
58
|
+
_MSG_OTHER = (
|
|
43
59
|
"\n\n"
|
|
44
|
-
" nedb._native is not available
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
" the
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
" 2. Re-install on a platform that has a native wheel:\n"
|
|
53
|
-
" pip install --force-reinstall --no-cache-dir nedb-engine\n\n"
|
|
54
|
-
" 3. Run 'nedbd --doctor' for a full diagnosis.\n"
|
|
60
|
+
" nedb._native (embedded v2 DAG core) is not available.\n"
|
|
61
|
+
" You have the universal wheel — reinstall to get the platform wheel:\n\n"
|
|
62
|
+
" pip install --force-reinstall --no-cache-dir nedb-engine\n\n"
|
|
63
|
+
" Or install the server binary and use HTTP mode (works everywhere):\n\n"
|
|
64
|
+
" cargo install nedb-engine # install nedbd v2 server\n"
|
|
65
|
+
" nedbd --dag ./data # start DAG server\n"
|
|
66
|
+
" NEDB_URL=http://localhost:7070 python3 your_script.py\n\n"
|
|
67
|
+
" Run 'nedbd --doctor' for a full diagnosis.\n"
|
|
55
68
|
)
|
|
69
|
+
_MSG = _MSG_MSYS2 if _is_msys2 else _MSG_OTHER
|
|
70
|
+
|
|
56
71
|
def __getattr__(self, name: str):
|
|
57
72
|
raise ImportError(f"nedb._native.{name} is not available.{self._MSG}")
|
|
58
73
|
|
|
@@ -71,6 +86,7 @@ __all__ = [
|
|
|
71
86
|
"MongoCompat", "MongoClient", "MongoError", "MongoUnsupportedError", "ObjectId",
|
|
72
87
|
"AutoIndexDB", "Sequencer",
|
|
73
88
|
"wrap_redis", "WrappedRedis",
|
|
89
|
+
"verify_proof", "fold_head",
|
|
74
90
|
"_native", "__has_native__",
|
|
75
91
|
]
|
|
76
|
-
__version__ = "2.2.
|
|
92
|
+
__version__ = "2.2.32"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nedb.proof — Merkle inclusion proofs for the hash-chained op log.
|
|
3
|
+
|
|
4
|
+
Every NEDB write produces an Op whose ``hash`` field is the chain head after that op
|
|
5
|
+
(``op.hash = blake2b(prev_head.utf8 + canon(body))`` — see ``nedb.log``). The chain
|
|
6
|
+
head therefore commits cryptographically to the entire write history.
|
|
7
|
+
|
|
8
|
+
This module derives a *parallel*, compact Merkle commitment chain over the same
|
|
9
|
+
op-hash sequence so a client can verify — offline, with no server round-trip and
|
|
10
|
+
without re-running the engine — that a specific op-hash was at a specific seq in
|
|
11
|
+
the log, and that the log's head followed from chaining all subsequent op-hashes
|
|
12
|
+
on top of it.
|
|
13
|
+
|
|
14
|
+
The chain step used by both ``verify_proof`` (client) and the server endpoint is::
|
|
15
|
+
|
|
16
|
+
head_next = blake2b(bytes.fromhex(head_prev) || bytes.fromhex(op_hash)).hexdigest()
|
|
17
|
+
|
|
18
|
+
starting from the genesis head (the all-zero 32-byte string). Folding over the
|
|
19
|
+
full ordered list of op-hashes yields a deterministic head; tamper with any op
|
|
20
|
+
hash (or reorder ops) and the fold no longer matches. The Op's own ``hash`` field
|
|
21
|
+
already commits to its body (payload, seq, nonce, ts, …) under the engine's chain
|
|
22
|
+
formula, so this fold is a complete commitment to the write history through the
|
|
23
|
+
op-hashes alone — exactly the property a Merkle inclusion proof needs.
|
|
24
|
+
|
|
25
|
+
The fold's terminal value (``proof["head"]``) is a *derived* head, distinct from
|
|
26
|
+
``db.head`` (the engine's UTF-8-hex chain) but a deterministic function of the
|
|
27
|
+
same log. The server computes the derived head over the live log when issuing
|
|
28
|
+
each proof, so any divergence between client fold and server fold means the
|
|
29
|
+
proof was tampered with in transit.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import hashlib
|
|
34
|
+
from typing import Dict, List
|
|
35
|
+
|
|
36
|
+
# Genesis head matches the engine: all-zero 32-byte string in hex.
|
|
37
|
+
GENESIS = "0" * 64
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _blake(data: bytes) -> str:
|
|
41
|
+
"""BLAKE2b-256 over raw bytes, returned as hex — same primitive the engine uses."""
|
|
42
|
+
return hashlib.blake2b(data, digest_size=32).hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _chain_step(head_hex: str, op_hash_hex: str) -> str:
|
|
46
|
+
"""One Merkle-chain step on raw bytes (decoded from hex).
|
|
47
|
+
|
|
48
|
+
Mirrors the user-facing spec exactly:
|
|
49
|
+
new_head = blake2b(prev_head_bytes || op_hash_bytes)
|
|
50
|
+
"""
|
|
51
|
+
return _blake(bytes.fromhex(head_hex) + bytes.fromhex(op_hash_hex))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fold_head(op_hashes: List[str], start: str = GENESIS) -> str:
|
|
55
|
+
"""Fold an ordered list of op-hashes into a Merkle-chain head.
|
|
56
|
+
|
|
57
|
+
Used by the server to produce ``proof["head"]`` for any given log state and
|
|
58
|
+
by tests to sanity-check that the fold is order-sensitive and deterministic.
|
|
59
|
+
"""
|
|
60
|
+
head = start
|
|
61
|
+
for h in op_hashes:
|
|
62
|
+
head = _chain_step(head, h)
|
|
63
|
+
return head
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def verify_proof(proof: Dict) -> bool:
|
|
67
|
+
"""Verify a Merkle inclusion proof client-side — no server, no engine needed.
|
|
68
|
+
|
|
69
|
+
``proof`` shape::
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
"hash": "<64-hex>", # the document op's content hash (op.hash)
|
|
73
|
+
"seq": N, # sequence number of this op in the log
|
|
74
|
+
"prev_head": "<64-hex>", # chain head BEFORE this op (= op.prev_hash)
|
|
75
|
+
"subsequent": ["<64-hex>"], # op-hashes of every op AFTER this one
|
|
76
|
+
"head": "<64-hex>", # claimed final (derived) chain head
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Verification (matches the spec):
|
|
80
|
+
1. ``head_at_seq = blake2b(prev_head_bytes || hash_bytes)``
|
|
81
|
+
2. For each ``sub_hash`` in subsequent:
|
|
82
|
+
``head = blake2b(head_bytes || sub_hash_bytes)``
|
|
83
|
+
3. ``final head == proof["head"]``
|
|
84
|
+
|
|
85
|
+
Returns True iff every step is internally consistent. Tampering with any
|
|
86
|
+
field — flipping a byte of ``hash``, reordering ``subsequent``, swapping
|
|
87
|
+
``prev_head``, etc. — will flip the fold and make this return False.
|
|
88
|
+
"""
|
|
89
|
+
if not isinstance(proof, dict):
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# Required keys with the right shapes.
|
|
93
|
+
required = ("hash", "seq", "prev_head", "subsequent", "head")
|
|
94
|
+
if not all(k in proof for k in required):
|
|
95
|
+
return False
|
|
96
|
+
if not isinstance(proof["seq"], int) or proof["seq"] < 0:
|
|
97
|
+
return False
|
|
98
|
+
if not isinstance(proof["subsequent"], list):
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
# Hex shape check on the 64-char fields. Anything malformed → not verified.
|
|
102
|
+
def _is_hex32(s: object) -> bool:
|
|
103
|
+
if not isinstance(s, str) or len(s) != 64:
|
|
104
|
+
return False
|
|
105
|
+
try:
|
|
106
|
+
bytes.fromhex(s)
|
|
107
|
+
except ValueError:
|
|
108
|
+
return False
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
if not _is_hex32(proof["hash"]): return False
|
|
112
|
+
if not _is_hex32(proof["prev_head"]): return False
|
|
113
|
+
if not _is_hex32(proof["head"]): return False
|
|
114
|
+
for s in proof["subsequent"]:
|
|
115
|
+
if not _is_hex32(s):
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
# 1. Re-derive the chain head AT this op's seq from prev_head + this op's hash.
|
|
119
|
+
head = _chain_step(proof["prev_head"], proof["hash"])
|
|
120
|
+
|
|
121
|
+
# 2. Fold each subsequent op-hash on top to advance the chain to the tail.
|
|
122
|
+
for sub in proof["subsequent"]:
|
|
123
|
+
head = _chain_step(head, sub)
|
|
124
|
+
|
|
125
|
+
# 3. The fold must equal the claimed final head bit-for-bit.
|
|
126
|
+
return head == proof["head"]
|
|
@@ -33,6 +33,7 @@ HTTP API (all JSON):
|
|
|
33
33
|
POST /v1/databases/<name>/files {name, data_b64, tier?} — store a file (Cascade-compressed)
|
|
34
34
|
GET /v1/databases/<name>/files/<filename>?version=N&tier=warm — retrieve a file
|
|
35
35
|
GET /v1/databases/<name>/files/<filename>/root?version=N&tier=warm — Merkle root (anchorable)
|
|
36
|
+
POST /v1/databases/<name>/proof {hash} — Merkle inclusion proof (verifiable offline)
|
|
36
37
|
POST /v1/databases/<name>/checkpoint — on-demand checkpoint
|
|
37
38
|
GET /v1/databases/<name>/batch — batch writes (array of {op,coll,id,doc})
|
|
38
39
|
"""
|
|
@@ -368,6 +369,57 @@ def make_handler(manager: Manager, token: Optional[str]):
|
|
|
368
369
|
ops = [o.to_dict() for o in db.log.ops[-limit:]][::-1]
|
|
369
370
|
self._send(200, {"log": ops, "seq": db.seq, "head": db.head})
|
|
370
371
|
return
|
|
372
|
+
# POST /v1/databases/<name>/proof
|
|
373
|
+
# Body: {"hash": "<64-hex>"} — the op.hash of a previously-written doc
|
|
374
|
+
# Response: a self-contained Merkle inclusion proof + a verified flag.
|
|
375
|
+
# The proof is verifiable client-side via nedb.verify_proof() with no
|
|
376
|
+
# server / engine — the server's role is only to look up the seq,
|
|
377
|
+
# collect the prev_head and subsequent op-hashes, and fold them into
|
|
378
|
+
# the same derived head the client will re-derive locally.
|
|
379
|
+
if method == "POST" and action == "proof":
|
|
380
|
+
from .proof import verify_proof as _vp, fold_head as _fold, GENESIS as _GEN
|
|
381
|
+
b = self._body()
|
|
382
|
+
want = str(b.get("hash", "")).strip().lower()
|
|
383
|
+
if not want or len(want) != 64:
|
|
384
|
+
raise HttpError(400, "hash (64-char hex) is required")
|
|
385
|
+
try:
|
|
386
|
+
bytes.fromhex(want)
|
|
387
|
+
except ValueError:
|
|
388
|
+
raise HttpError(400, "hash must be 64-char hexadecimal")
|
|
389
|
+
# Find the Op with this exact chain-hash.
|
|
390
|
+
ops = db.log.ops
|
|
391
|
+
target = None
|
|
392
|
+
for o in ops:
|
|
393
|
+
if o.hash == want:
|
|
394
|
+
target = o
|
|
395
|
+
break
|
|
396
|
+
if target is None:
|
|
397
|
+
raise HttpError(404, f"no op with hash={want[:16]}…")
|
|
398
|
+
# The proof scheme is a Merkle fold over op-hashes — a
|
|
399
|
+
# PARALLEL commitment to the engine's per-op chain. Both
|
|
400
|
+
# are deterministic functions of the same log. Replay the
|
|
401
|
+
# fold from GENESIS to determine the fold-value just before
|
|
402
|
+
# this op (prev_head), then collect every subsequent op-hash
|
|
403
|
+
# so the client can fold forward to the same derived head.
|
|
404
|
+
op_hashes = [o.hash for o in ops]
|
|
405
|
+
prev_head = _fold(op_hashes[: target.seq], start=_GEN)
|
|
406
|
+
subsequent = op_hashes[target.seq + 1:]
|
|
407
|
+
derived_head = _fold(op_hashes, start=_GEN)
|
|
408
|
+
proof = {
|
|
409
|
+
"hash": target.hash,
|
|
410
|
+
"seq": target.seq,
|
|
411
|
+
"prev_head": prev_head,
|
|
412
|
+
"subsequent": subsequent,
|
|
413
|
+
"head": derived_head,
|
|
414
|
+
}
|
|
415
|
+
# Self-check: confirm the proof verifies before returning it,
|
|
416
|
+
# so a 200 from this endpoint is a hard guarantee of soundness.
|
|
417
|
+
proof["verified"] = _vp(proof)
|
|
418
|
+
# Helpful extras for clients that want to anchor to db.head too.
|
|
419
|
+
proof["engine_head"] = db.head
|
|
420
|
+
proof["engine_seq"] = db.seq
|
|
421
|
+
self._send(200, proof)
|
|
422
|
+
return
|
|
371
423
|
# MongoDB-compatible query endpoint
|
|
372
424
|
# POST /v1/databases/<name>/mongo
|
|
373
425
|
# Body: {collection, op, ...op-specific args}
|
|
@@ -676,7 +728,7 @@ def _run_doctor() -> None: # noqa: C901
|
|
|
676
728
|
step += 1
|
|
677
729
|
if has_cargo:
|
|
678
730
|
_cmd("install from crates.io (recommended)",
|
|
679
|
-
"cargo install nedb-
|
|
731
|
+
"cargo install nedb-engine",
|
|
680
732
|
f"binary → {os.path.join(_cargo_bin, 'nedbd')}")
|
|
681
733
|
print()
|
|
682
734
|
_cmd("OR reinstall pip wheel (also bundles the binary)",
|
|
@@ -810,7 +862,7 @@ def main() -> None:
|
|
|
810
862
|
print("", file=_sys.stderr)
|
|
811
863
|
print(" Quick fixes:", file=_sys.stderr)
|
|
812
864
|
print(" pip install --force-reinstall --no-cache-dir nedb-engine # re-install with binary", file=_sys.stderr)
|
|
813
|
-
print(" cargo install nedb-
|
|
865
|
+
print(" cargo install nedb-engine # build from source (any platform)", file=_sys.stderr)
|
|
814
866
|
print("", file=_sys.stderr)
|
|
815
867
|
_sys.exit(1)
|
|
816
868
|
# Rust binary: nedbd-v2 [data_dir]
|
|
@@ -861,6 +913,30 @@ def main() -> None:
|
|
|
861
913
|
print(BANNER)
|
|
862
914
|
level_label = {0: "errors-only", 1: "requests", 2: "deploy", 3: "verbose"}.get(_log_level, str(_log_level))
|
|
863
915
|
print(f" nedbd {__version__} — http://{host}:{port} data={os.path.abspath(data)} auth={auth} log={level_label}")
|
|
916
|
+
|
|
917
|
+
# ── v2 DAG health notice ──────────────────────────────────────────────────
|
|
918
|
+
# Check once at startup whether the v2 binary and _native extension are available.
|
|
919
|
+
# Print a single actionable line if either is missing — non-blocking, no exit.
|
|
920
|
+
_has_native = False
|
|
921
|
+
try:
|
|
922
|
+
from nedb import __has_native__ as _has_native # type: ignore[attr-defined]
|
|
923
|
+
except Exception:
|
|
924
|
+
pass
|
|
925
|
+
_dag_bin_available = shutil.which("nedbd-v2") or shutil.which("nedbd_v2") or \
|
|
926
|
+
any(os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), n))
|
|
927
|
+
for n in ["nedbd-v2", "nedbd-v2.exe", "nedbd_v2", "nedbd_v2.exe"])
|
|
928
|
+
if not _has_native or not _dag_bin_available:
|
|
929
|
+
print()
|
|
930
|
+
if not _dag_bin_available:
|
|
931
|
+
print(" ⚡ v2 DAG engine not found — for content-addressed, instant-cold-start mode:")
|
|
932
|
+
print(" cargo install nedb-engine → nedbd --dag ./data")
|
|
933
|
+
if not _has_native:
|
|
934
|
+
print(" ⚡ nedb._native not available — embedded v2 API requires the platform wheel")
|
|
935
|
+
print(" pip install --force-reinstall --no-cache-dir nedb-engine")
|
|
936
|
+
print(" OR use HTTP mode: NEDB_URL=http://localhost:7070 python3 your_script.py")
|
|
937
|
+
if not _has_native or not _dag_bin_available:
|
|
938
|
+
print(" ℹ Run 'nedbd --doctor' for a full diagnosis and exact fix commands.")
|
|
939
|
+
print()
|
|
864
940
|
# Eagerly open all known databases so backfill-encrypt (if needed) runs
|
|
865
941
|
# immediately and is visible in the boot log, not hidden on first request.
|
|
866
942
|
names = manager.names()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# nedb-engine
|
|
2
|
+
|
|
3
|
+
**NEDB v2 — content-addressed DAG storage engine with NQL and HTTP server**
|
|
4
|
+
|
|
5
|
+
[](https://crates.io/crates/nedb-engine)
|
|
6
|
+
[](https://github.com/aiassistsecure/nedb/blob/master/LICENSE)
|
|
7
|
+
[](https://github.com/aiassistsecure/nedb)
|
|
8
|
+
|
|
9
|
+
This crate ships the **`nedbd`** binary — the NEDB v2 DAG HTTP server. Install it, point it at a data directory, and any language can speak to it over HTTP/JSON.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cargo install nedb-engine
|
|
13
|
+
nedbd ./data # AOF engine (pure Rust, v1-compatible)
|
|
14
|
+
nedbd --dag ./data # DAG engine (v2, content-addressed, recommended)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## What is NEDB v2?
|
|
20
|
+
|
|
21
|
+
NEDB v2 replaces the append-only log (AOF) with a **content-addressed Merkle DAG**:
|
|
22
|
+
|
|
23
|
+
- Every document version is an **immutable, BLAKE2b-hashed object**. Nothing is ever overwritten.
|
|
24
|
+
- Every write produces a new **chain head** — a BLAKE2b commitment over the entire database history.
|
|
25
|
+
- **Time-travel**: read any document `AS OF seq N` to see exactly what it contained at that point.
|
|
26
|
+
- **Causal provenance**: documents link to their causal parents via `caused_by` hashes. `TRACE caused_by` walks the full causal graph backward.
|
|
27
|
+
- **TRAVERSE**: named graph relations via `__links__`. `FROM person WHERE _id = "robert" TRAVERSE parent_of` returns linked nodes.
|
|
28
|
+
- **O(1) warm start**: a `MANIFEST` file stores `seq` + Merkle head so restarts never re-scan the entire object store.
|
|
29
|
+
- **Instant cold start**: the daemon accepts connections immediately; background scan loads objects incrementally.
|
|
30
|
+
- **AES-256-GCM at rest**: optional symmetric encryption with a double-envelope key structure (TMK wraps DEK).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cargo install nedb-engine
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `nedbd` binary lands in `~/.cargo/bin/`. Make sure that's on your `PATH`.
|
|
41
|
+
|
|
42
|
+
To verify:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
nedbd --doctor # diagnose your NEDB environment
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
nedbd [OPTIONS] [data_dir]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Argument | Default | Description |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `data_dir` | `./nedb-data` | Directory for database files |
|
|
59
|
+
| `--dag` | off | Use the v2 content-addressed DAG engine |
|
|
60
|
+
| `--doctor` | — | Diagnose environment, print fix commands |
|
|
61
|
+
|
|
62
|
+
### Environment variables
|
|
63
|
+
|
|
64
|
+
| Variable | Default | Description |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `NEDBD_HOST` | `127.0.0.1` | Bind address |
|
|
67
|
+
| `NEDBD_PORT` | `7070` | HTTP port |
|
|
68
|
+
| `NEDBD_TOKEN` | — | Bearer token for auth (optional) |
|
|
69
|
+
| `NEDB_TMK` | — | 64-char hex master key for AES-256-GCM encryption at rest |
|
|
70
|
+
| `NEDBD_MEMORY` | — | `1` = pure in-memory mode (no disk I/O) |
|
|
71
|
+
| `NEDBD_DAG` | — | `1` = same as `--dag` flag |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## HTTP API
|
|
76
|
+
|
|
77
|
+
All endpoints return JSON. Auth: `Authorization: Bearer <token>` if `NEDBD_TOKEN` is set.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
GET /health
|
|
81
|
+
GET /v1/databases
|
|
82
|
+
POST /v1/databases {name, init?}
|
|
83
|
+
GET /v1/databases/<name>
|
|
84
|
+
DELETE /v1/databases/<name>
|
|
85
|
+
POST /v1/databases/<name>/put {coll, id, doc, caused_by?}
|
|
86
|
+
POST /v1/databases/<name>/query {nql}
|
|
87
|
+
POST /v1/databases/<name>/link {frm, rel, to}
|
|
88
|
+
POST /v1/databases/<name>/neighbors {node, rel}
|
|
89
|
+
GET /v1/databases/<name>/verify
|
|
90
|
+
POST /v1/databases/<name>/checkpoint
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## NQL — NEDB Query Language
|
|
96
|
+
|
|
97
|
+
```sql
|
|
98
|
+
-- Basic queries
|
|
99
|
+
FROM person LIMIT 10
|
|
100
|
+
FROM driver WHERE status = "active"
|
|
101
|
+
FROM driver WHERE rating >= 4.5 ORDER BY rating DESC
|
|
102
|
+
|
|
103
|
+
-- Time-travel
|
|
104
|
+
FROM will WHERE _id = "evans_will_2019" AS OF 5
|
|
105
|
+
|
|
106
|
+
-- Causal trace (walks caused_by links backward)
|
|
107
|
+
FROM event WHERE _id = "probate_filing" TRACE caused_by
|
|
108
|
+
|
|
109
|
+
-- Graph traversal
|
|
110
|
+
FROM person WHERE _id = "robert" TRAVERSE parent_of
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Example: The Will — causal DAG in action
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import json, os, urllib.request
|
|
119
|
+
|
|
120
|
+
def put(db_url, coll, id_, doc):
|
|
121
|
+
body = json.dumps({"coll": coll, "id": id_, "doc": doc}).encode()
|
|
122
|
+
req = urllib.request.Request(f"{db_url}/put", body,
|
|
123
|
+
{"Content-Type": "application/json"}, method="POST")
|
|
124
|
+
return json.loads(urllib.request.urlopen(req).read())["doc"]
|
|
125
|
+
|
|
126
|
+
BASE = "http://localhost:7070/v1/databases/will"
|
|
127
|
+
|
|
128
|
+
# Robert writes his will
|
|
129
|
+
robert = put(BASE, "person", "robert", {"name": "Robert Evans", "role": "testator"})
|
|
130
|
+
will_v1 = put(BASE, "will", "evans_will", {
|
|
131
|
+
"house": "mark", "business": "lisa",
|
|
132
|
+
"caused_by": [robert["_hash"]], # causal link to testator record
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
# Amendment — chains off v1
|
|
136
|
+
will_v2 = put(BASE, "will", "evans_will", {
|
|
137
|
+
"house": "mark", "business": "lisa", "vintage_car": "mark",
|
|
138
|
+
"caused_by": [will_v1["_hash"]],
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
# TRACE proves the amendment links back to the original
|
|
142
|
+
query = json.dumps({"nql": 'FROM will WHERE _id = "evans_will" TRACE caused_by'}).encode()
|
|
143
|
+
req = urllib.request.Request(f"{BASE}/query", query,
|
|
144
|
+
{"Content-Type": "application/json"}, method="POST")
|
|
145
|
+
trace = json.loads(urllib.request.urlopen(req).read())["rows"]
|
|
146
|
+
# → both versions + Robert's testator record, in causal order
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Python companion
|
|
152
|
+
|
|
153
|
+
The `nedb-engine` PyPI package ships the same server binary bundled in the wheel, plus:
|
|
154
|
+
- Pure-Python AOF engine (`NEDB` class)
|
|
155
|
+
- Embedded v2 DAG API (`nedb._native.NedbCore`) on supported platforms
|
|
156
|
+
- `nedbd` console script
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pip install nedb-engine
|
|
160
|
+
|
|
161
|
+
# Use embedded API (Linux/macOS/Windows CPython)
|
|
162
|
+
python3 -c "from nedb._native import NedbCore; db = NedbCore(); ..."
|
|
163
|
+
|
|
164
|
+
# Use HTTP mode (any platform, including MSYS2/MinGW)
|
|
165
|
+
NEDB_URL=http://localhost:7070 python3 your_script.py
|
|
166
|
+
|
|
167
|
+
# Diagnose
|
|
168
|
+
nedbd --doctor
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
[BUSL-1.1](https://github.com/aiassistsecure/nedb/blob/master/LICENSE) — Business Source License. Free for development and evaluation; production use requires a commercial licence from INTERCHAINED, LLC.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
*Built by [INTERCHAINED, LLC](https://interchained.org) × Claude Sonnet 4.6*
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|