Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions engram-ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
.DS_Store
*.log
100 changes: 100 additions & 0 deletions engram-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# @engram/client

TypeScript SDK for [Engram](https://github.com/Dipraise1/Engram) — a decentralized vector database on Bittensor.

Mirrors the [Python SDK](https://github.com/Dipraise1/Engram/tree/main/engram/sdk) API.

## Installation

```bash
npm install @engram/client
```

## Quick Start

```typescript
import { EngramClient } from "@engram/client";

const client = new EngramClient({ minerUrl: "http://127.0.0.1:8091" });

// Store a memory
const cid = await client.ingest("Transformers changed deep learning.");
console.log("Stored as:", cid);

// Search
const results = await client.query("deep learning", 5);
for (const r of results) {
console.log(r.cid, r.score, r.metadata);
}
```

## API

### EngramClient

| Method | Description |
|--------|-------------|
| `ingest(text, metadata?)` | Embed and store text |
| `ingestEmbedding(embedding, metadata?)` | Store a pre-computed vector |
| `query(text, topK?, filter?)` | Semantic search |
| `queryByVector(vector, topK?)` | Search by vector |
| `get(cid)` | Retrieve by CID |
| `delete(cid)` | Delete by CID |
| `list(filter?, limit?, offset?)` | List records |
| `health()` | Miner liveness check |
| `isOnline()` | Returns boolean |
| `batchIngestFile(path)` | Ingest from JSONL |
| `ingestUrl(url)` | Fetch and store URL |
| `ingestConversation(messages)` | Store conversation |

### Encryption

```typescript
import { generateKeypair, HybridEncryption, NamespaceEncryption } from "@engram/client";

// X25519 + HKDF + AES-256-GCM (recommended)
const [priv, pub] = generateKeypair();
const enc = new HybridEncryption({ privateKey: priv });
const blob = await enc.encryptPayload("secret", { tags: ["demo"] });
const [text, meta] = await enc.decryptPayload(blob);

// Password-based (legacy)
const enc2 = await NamespaceEncryption.create("my-ns", "my-key");
```

### Shamir Secret Sharing

```typescript
import { splitSecret, reconstructSecret } from "@engram/client";

const secret = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
const shares = splitSecret(secret, 2, 3); // 2-of-3 threshold

const recovered = reconstructSecret(shares.slice(0, 2));
```

### Error Handling

```typescript
import { EngramError, MinerOfflineError, IngestError, QueryError } from "@engram/client";

try {
await client.ingest("hello");
} catch (err) {
if (err instanceof MinerOfflineError) {
console.log("Miner unreachable:", err.url);
}
}
```

## Development

```bash
npm install
npm run build
npm test
```

## License

MIT
27 changes: 27 additions & 0 deletions engram-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@engram/client",
"version": "0.1.0",
"description": "TypeScript SDK for Engram",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"test": "jest --passWithNoTests"
},
"license": "MIT",
"dependencies": {
"@noble/ciphers": "^1.2.1",
"@noble/curves": "^1.8.1",
"@noble/hashes": "^1.7.1"
},
"optionalDependencies": {
"@polkadot/util-crypto": "^12.6.2"
},
"devDependencies": {
"@types/node": "^22.13.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.0",
"typescript": "^5.7.0"
}
}
65 changes: 65 additions & 0 deletions engram-ts/src/__tests__/shamir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { splitSecret, reconstructSecret } from "../shamir.js";

describe("ShamirSecretSharing", () => {
it("should split and reconstruct a simple secret with 2-of-3", () => {
const secret = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
const shares = splitSecret(secret, 2, 3);
expect(shares).toHaveLength(3);

// Reconstruct with first 2 shares
const recovered = reconstructSecret(shares.slice(0, 2));
expect(recovered).toEqual(secret);
});

it("should reconstruct with 3-of-5", () => {
const secret = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]);
const shares = splitSecret(secret, 3, 5);
expect(shares).toHaveLength(5);

const recovered = reconstructSecret(shares.slice(0, 3));
expect(recovered).toEqual(secret);
});

it("should reconstruct with any subset of shares (any k of n)", () => {
const secret = new Uint8Array([0xca, 0xfe]);
const shares = splitSecret(secret, 2, 5);

// Try different combinations
const r1 = reconstructSecret([shares[0], shares[4]]);
expect(r1).toEqual(secret);

const r2 = reconstructSecret([shares[2], shares[3]]);
expect(r2).toEqual(secret);
});

it("should throw with insufficient shares", () => {
const secret = new Uint8Array([0x12, 0x34]);
const shares = splitSecret(secret, 3, 5);

expect(() => reconstructSecret(shares.slice(0, 2))).toThrow();
});

it("should throw with empty secret", () => {
expect(() => splitSecret(new Uint8Array(0), 2, 3)).toThrow();
});

it("should throw when threshold > total", () => {
expect(() => splitSecret(new Uint8Array([0x01]), 4, 3)).toThrow();
});

it("should handle single-byte secret", () => {
const secret = new Uint8Array([0xff]);
const shares = splitSecret(secret, 2, 3);
const recovered = reconstructSecret(shares.slice(0, 2));
expect(recovered).toEqual(secret);
});

it("should handle 32-byte secret (e.g., AES key)", () => {
const secret = new Uint8Array(32);
for (let i = 0; i < 32; i++) secret[i] = i;

const shares = splitSecret(secret, 2, 3);
const recovered = reconstructSecret(shares.slice(0, 2));
expect(recovered).toEqual(secret);
});
});
Loading