diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index eaeb43d..a4c25fa 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"isar_flutter_libs","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\isar_flutter_libs-3.1.0+1\\\\","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"isar_flutter_libs","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\isar_flutter_libs-3.1.0+1\\\\","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_android","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.17\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"isar_flutter_libs","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\isar_flutter_libs-3.1.0+1\\\\","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"isar_flutter_libs","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\isar_flutter_libs-3.1.0+1\\\\","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_linux","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"isar_flutter_libs","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\isar_flutter_libs-3.1.0+1\\\\","native_build":true,"dependencies":[],"dev_dependency":true},{"name":"path_provider_windows","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2025-08-16 18:52:05.168876","version":"3.32.6","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"onnxruntime","path":"/home/jules/.pub-cache/hosted/pub.dev/onnxruntime-1.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"onnxruntime","path":"/home/jules/.pub-cache/hosted/pub.dev/onnxruntime-1.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"onnxruntime","path":"/home/jules/.pub-cache/hosted/pub.dev/onnxruntime-1.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"onnxruntime","path":"/home/jules/.pub-cache/hosted/pub.dev/onnxruntime-1.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"onnxruntime","path":"/home/jules/.pub-cache/hosted/pub.dev/onnxruntime-1.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"onnxruntime","dependencies":[]}],"date_created":"2025-11-24 21:02:48.717549","version":"3.38.3","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/flutter b/flutter new file mode 160000 index 0000000..19074d1 --- /dev/null +++ b/flutter @@ -0,0 +1 @@ +Subproject commit 19074d12f7eaf6a8180cd4036a430c1d76de904e diff --git a/isar_agent_memory_tests/linux/flutter/generated_plugin_registrant.cc b/isar_agent_memory_tests/linux/flutter/generated_plugin_registrant.cc index e71a16d..b898c8c 100644 --- a/isar_agent_memory_tests/linux/flutter/generated_plugin_registrant.cc +++ b/isar_agent_memory_tests/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); + isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); } diff --git a/isar_agent_memory_tests/linux/flutter/generated_plugins.cmake b/isar_agent_memory_tests/linux/flutter/generated_plugins.cmake index 2e1de87..61d31f0 100644 --- a/isar_agent_memory_tests/linux/flutter/generated_plugins.cmake +++ b/isar_agent_memory_tests/linux/flutter/generated_plugins.cmake @@ -3,9 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + isar_flutter_libs ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + onnxruntime ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/isar_agent_memory_tests/macos/Flutter/GeneratedPluginRegistrant.swift b/isar_agent_memory_tests/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..692f4a0 100644 --- a/isar_agent_memory_tests/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/isar_agent_memory_tests/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import isar_flutter_libs func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) } diff --git a/isar_agent_memory_tests/pubspec.yaml b/isar_agent_memory_tests/pubspec.yaml index 8f914ef..5bebde9 100644 --- a/isar_agent_memory_tests/pubspec.yaml +++ b/isar_agent_memory_tests/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + isar_flutter_libs: ^3.1.0+1 dev_dependencies: flutter_test: diff --git a/isar_agent_memory_tests/windows/flutter/generated_plugin_registrant.cc b/isar_agent_memory_tests/windows/flutter/generated_plugin_registrant.cc index 8b6d468..afc39a1 100644 --- a/isar_agent_memory_tests/windows/flutter/generated_plugin_registrant.cc +++ b/isar_agent_memory_tests/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + IsarFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); } diff --git a/isar_agent_memory_tests/windows/flutter/generated_plugins.cmake b/isar_agent_memory_tests/windows/flutter/generated_plugins.cmake index b93c4c3..9f4a46c 100644 --- a/isar_agent_memory_tests/windows/flutter/generated_plugins.cmake +++ b/isar_agent_memory_tests/windows/flutter/generated_plugins.cmake @@ -3,9 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + isar_flutter_libs ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + onnxruntime ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index 41e2eb6..792173b 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -17,9 +17,9 @@ { "id": "2:1442387009774653075", "name": "docKey", + "indexId": "1:8701917581695520620", "type": 9, - "flags": 2080, - "indexId": "1:8701917581695520620" + "flags": 2080 }, { "id": "3:1210557137860714142", @@ -29,9 +29,9 @@ { "id": "4:9089606705394925499", "name": "vector", + "indexId": "2:7396986783401268781", "type": 28, - "flags": 8, - "indexId": "2:7396986783401268781" + "flags": 8 } ], "relations": [] diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index f07e7ac..abfe5fd 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -19,40 +19,42 @@ export 'package:objectbox/objectbox.dart'; // so that callers only have to impor final _entities = [ obx_int.ModelEntity( - id: const obx_int.IdUid(1, 1718053728221939704), - name: 'ObxVectorDoc', - lastPropertyId: const obx_int.IdUid(4, 9089606705394925499), - flags: 0, - properties: [ - obx_int.ModelProperty( - id: const obx_int.IdUid(1, 722458666947821368), - name: 'id', - type: 6, - flags: 1), - obx_int.ModelProperty( - id: const obx_int.IdUid(2, 1442387009774653075), - name: 'docKey', - type: 9, - flags: 2080, - indexId: const obx_int.IdUid(1, 8701917581695520620)), - obx_int.ModelProperty( - id: const obx_int.IdUid(3, 1210557137860714142), - name: 'content', - type: 9, - flags: 0), - obx_int.ModelProperty( - id: const obx_int.IdUid(4, 9089606705394925499), - name: 'vector', - type: 28, - flags: 8, - indexId: const obx_int.IdUid(2, 7396986783401268781), - hnswParams: obx_int.ModelHnswParams( - dimensions: 768, - distanceType: 2, - )) - ], - relations: [], - backlinks: []) + id: const obx_int.IdUid(1, 1718053728221939704), + name: 'ObxVectorDoc', + lastPropertyId: const obx_int.IdUid(4, 9089606705394925499), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 722458666947821368), + name: 'id', + type: 6, + flags: 1, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 1442387009774653075), + name: 'docKey', + type: 9, + flags: 2080, + indexId: const obx_int.IdUid(1, 8701917581695520620), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 1210557137860714142), + name: 'content', + type: 9, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 9089606705394925499), + name: 'vector', + type: 28, + flags: 8, + indexId: const obx_int.IdUid(2, 7396986783401268781), + hnswParams: obx_int.ModelHnswParams(dimensions: 768, distanceType: 2), + ), + ], + relations: [], + backlinks: [], + ), ]; /// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter @@ -66,81 +68,92 @@ final _entities = [ /// For Flutter apps, also calls `loadObjectBoxLibraryAndroidCompat()` from /// the ObjectBox Flutter library to fix loading the native ObjectBox library /// on Android 6 and older. -obx.Store openStore( - {String? directory, - int? maxDBSizeInKB, - int? maxDataSizeInKB, - int? fileMode, - int? maxReaders, - bool queriesCaseSensitiveDefault = true, - String? macosApplicationGroup}) { - return obx.Store(getObjectBoxModel(), - directory: directory, - maxDBSizeInKB: maxDBSizeInKB, - maxDataSizeInKB: maxDataSizeInKB, - fileMode: fileMode, - maxReaders: maxReaders, - queriesCaseSensitiveDefault: queriesCaseSensitiveDefault, - macosApplicationGroup: macosApplicationGroup); +obx.Store openStore({ + String? directory, + int? maxDBSizeInKB, + int? maxDataSizeInKB, + int? fileMode, + int? maxReaders, + bool queriesCaseSensitiveDefault = true, + String? macosApplicationGroup, +}) { + return obx.Store( + getObjectBoxModel(), + directory: directory, + maxDBSizeInKB: maxDBSizeInKB, + maxDataSizeInKB: maxDataSizeInKB, + fileMode: fileMode, + maxReaders: maxReaders, + queriesCaseSensitiveDefault: queriesCaseSensitiveDefault, + macosApplicationGroup: macosApplicationGroup, + ); } /// Returns the ObjectBox model definition for this project for use with /// [obx.Store.new]. obx_int.ModelDefinition getObjectBoxModel() { final model = obx_int.ModelInfo( - entities: _entities, - lastEntityId: const obx_int.IdUid(1, 1718053728221939704), - lastIndexId: const obx_int.IdUid(2, 7396986783401268781), - lastRelationId: const obx_int.IdUid(0, 0), - lastSequenceId: const obx_int.IdUid(0, 0), - retiredEntityUids: const [], - retiredIndexUids: const [], - retiredPropertyUids: const [], - retiredRelationUids: const [], - modelVersion: 5, - modelVersionParserMinimum: 5, - version: 1); + entities: _entities, + lastEntityId: const obx_int.IdUid(1, 1718053728221939704), + lastIndexId: const obx_int.IdUid(2, 7396986783401268781), + lastRelationId: const obx_int.IdUid(0, 0), + lastSequenceId: const obx_int.IdUid(0, 0), + retiredEntityUids: const [], + retiredIndexUids: const [], + retiredPropertyUids: const [], + retiredRelationUids: const [], + modelVersion: 5, + modelVersionParserMinimum: 5, + version: 1, + ); final bindings = { ObxVectorDoc: obx_int.EntityDefinition( - model: _entities[0], - toOneRelations: (ObxVectorDoc object) => [], - toManyRelations: (ObxVectorDoc object) => {}, - getId: (ObxVectorDoc object) => object.id, - setId: (ObxVectorDoc object, int id) { - object.id = id; - }, - objectToFB: (ObxVectorDoc object, fb.Builder fbb) { - final docKeyOffset = fbb.writeString(object.docKey); - final contentOffset = - object.content == null ? null : fbb.writeString(object.content!); - final vectorOffset = object.vector == null - ? null - : fbb.writeListFloat32(object.vector!); - fbb.startTable(5); - fbb.addInt64(0, object.id); - fbb.addOffset(1, docKeyOffset); - fbb.addOffset(2, contentOffset); - fbb.addOffset(3, vectorOffset); - fbb.finish(fbb.endTable()); - return object.id; - }, - objectFromFB: (obx.Store store, ByteData fbData) { - final buffer = fb.BufferContext(fbData); - final rootOffset = buffer.derefObject(0); - final docKeyParam = const fb.StringReader(asciiOptimization: true) - .vTableGet(buffer, rootOffset, 6, ''); - final contentParam = const fb.StringReader(asciiOptimization: true) - .vTableGetNullable(buffer, rootOffset, 8); - final vectorParam = - const fb.ListReader(fb.Float32Reader(), lazy: false) - .vTableGetNullable(buffer, rootOffset, 10); - final object = ObxVectorDoc( - docKey: docKeyParam, content: contentParam, vector: vectorParam) - ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + model: _entities[0], + toOneRelations: (ObxVectorDoc object) => [], + toManyRelations: (ObxVectorDoc object) => {}, + getId: (ObxVectorDoc object) => object.id, + setId: (ObxVectorDoc object, int id) { + object.id = id; + }, + objectToFB: (ObxVectorDoc object, fb.Builder fbb) { + final docKeyOffset = fbb.writeString(object.docKey); + final contentOffset = object.content == null + ? null + : fbb.writeString(object.content!); + final vectorOffset = object.vector == null + ? null + : fbb.writeListFloat32(object.vector!); + fbb.startTable(5); + fbb.addInt64(0, object.id); + fbb.addOffset(1, docKeyOffset); + fbb.addOffset(2, contentOffset); + fbb.addOffset(3, vectorOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final docKeyParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 6, ''); + final contentParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGetNullable(buffer, rootOffset, 8); + final vectorParam = const fb.ListReader( + fb.Float32Reader(), + lazy: false, + ).vTableGetNullable(buffer, rootOffset, 10); + final object = ObxVectorDoc( + docKey: docKeyParam, + content: contentParam, + vector: vectorParam, + )..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); - return object; - }) + return object; + }, + ), }; return obx_int.ModelDefinition(model, bindings); @@ -149,18 +162,22 @@ obx_int.ModelDefinition getObjectBoxModel() { /// [ObxVectorDoc] entity fields to define ObjectBox queries. class ObxVectorDoc_ { /// See [ObxVectorDoc.id]. - static final id = - obx.QueryIntegerProperty(_entities[0].properties[0]); + static final id = obx.QueryIntegerProperty( + _entities[0].properties[0], + ); /// See [ObxVectorDoc.docKey]. - static final docKey = - obx.QueryStringProperty(_entities[0].properties[1]); + static final docKey = obx.QueryStringProperty( + _entities[0].properties[1], + ); /// See [ObxVectorDoc.content]. - static final content = - obx.QueryStringProperty(_entities[0].properties[2]); + static final content = obx.QueryStringProperty( + _entities[0].properties[2], + ); /// See [ObxVectorDoc.vector]. - static final vector = - obx.QueryHnswProperty(_entities[0].properties[3]); + static final vector = obx.QueryHnswProperty( + _entities[0].properties[3], + ); } diff --git a/lib/src/models/memory_edge.dart b/lib/src/models/memory_edge.dart index e81480c..5abd336 100644 --- a/lib/src/models/memory_edge.dart +++ b/lib/src/models/memory_edge.dart @@ -17,7 +17,13 @@ class MemoryEdge { required this.relation, this.weight, this.metadata, - }) : createdAt = DateTime.now(); + this.version, + this.deviceId, + this.isDeleted = false, + DateTime? modifiedAt, + }) : createdAt = DateTime.now() { + this.modifiedAt = modifiedAt ?? DateTime.now(); + } /// Unique identifier for this edge, managed by Isar. Id id = Isar.autoIncrement; @@ -44,6 +50,20 @@ class MemoryEdge { /// Automatically set to the current time upon creation. late DateTime createdAt; + /// The timestamp when this record was last modified (system-level sync). + /// + /// Used for Last-Write-Wins (LWW) conflict resolution. + late DateTime modifiedAt; + + /// Version identifier (e.g., hash or monotonic counter) for synchronization. + String? version; + + /// ID of the device that last modified this record. + String? deviceId; + + /// Soft delete flag (tombstone) for synchronization. + bool isDeleted; + /// Arbitrary extensible metadata. /// /// A map for storing additional, non-indexed data. This field is ignored by diff --git a/lib/src/models/memory_node.dart b/lib/src/models/memory_node.dart index d02bf2b..34db68f 100644 --- a/lib/src/models/memory_node.dart +++ b/lib/src/models/memory_node.dart @@ -22,8 +22,13 @@ class MemoryNode { this.embedding, Degree? degree, this.metadata, + this.version, + this.deviceId, + this.isDeleted = false, + DateTime? modifiedAt, }) : createdAt = DateTime.now() { this.degree = degree ?? Degree(); + this.modifiedAt = modifiedAt ?? DateTime.now(); } /// Unique identifier for this node, managed by Isar. @@ -44,11 +49,25 @@ class MemoryNode { /// Automatically set to the current time upon creation. late DateTime createdAt; - /// The timestamp of the last update or access. + /// The timestamp of the last update or access (business logic update). /// /// Can be used to track recency and relevance. DateTime? updatedAt; + /// The timestamp when this record was last modified (system-level sync). + /// + /// Used for Last-Write-Wins (LWW) conflict resolution. + late DateTime modifiedAt; + + /// Version identifier (e.g., hash or monotonic counter) for synchronization. + String? version; + + /// ID of the device that last modified this record. + String? deviceId; + + /// Soft delete flag (tombstone) for synchronization. + bool isDeleted; + /// The embedding vector representing the semantic meaning of the [content]. /// /// This is used for semantic search and similarity comparisons. diff --git a/lib/src/sync/encryption_service.dart b/lib/src/sync/encryption_service.dart new file mode 100644 index 0000000..ca163e3 --- /dev/null +++ b/lib/src/sync/encryption_service.dart @@ -0,0 +1,55 @@ +import 'package:cryptography/cryptography.dart'; + +class EncryptionService { + // Change explicit Algorithm type to specific AesGcm or dynamic if base class is different/private + final AesGcm _algorithm = AesGcm.with256bits(); + SecretKey? _secretKey; + + bool get isInitialized => _secretKey != null; + + /// Initializes the service with a raw 32-byte key or generates one if not provided. + Future initialize({List? rawKey}) async { + if (rawKey != null) { + if (rawKey.length != 32) { + throw ArgumentError('Key must be 32 bytes long'); + } + _secretKey = await _algorithm.newSecretKeyFromBytes(rawKey); + } else { + _secretKey = await _algorithm.newSecretKey(); + } + } + + /// Encrypts a string using AES-256-GCM. + /// Returns a concatenation of nonce + ciphertext + mac for storage. + Future> encrypt(String plainText) async { + if (_secretKey == null) throw StateError('EncryptionService not initialized'); + + final secretBox = await _algorithm.encrypt( + plainText.codeUnits, + secretKey: _secretKey!, + ); + + return secretBox.concatenation(); + } + + /// Decrypts a byte list (nonce + ciphertext + mac) back to string. + Future decrypt(List data) async { + if (_secretKey == null) throw StateError('EncryptionService not initialized'); + + // AES-GCM in cryptography package: + // Nonce: 12 bytes (default for AesGcm) + // MAC: 16 bytes (default) + final secretBox = SecretBox.fromConcatenation( + data, + nonceLength: 12, + macLength: 16, + ); + + final clearTextBytes = await _algorithm.decrypt( + secretBox, + secretKey: _secretKey!, + ); + + return String.fromCharCodes(clearTextBytes); + } +} diff --git a/lib/src/sync/sync_manager.dart b/lib/src/sync/sync_manager.dart new file mode 100644 index 0000000..a5b135e --- /dev/null +++ b/lib/src/sync/sync_manager.dart @@ -0,0 +1,21 @@ +import 'package:isar_agent_memory/src/sync/encryption_service.dart'; + +class SyncManager { + final EncryptionService _encryptionService; + + // TODO: Inject SyncBackend (Firebase, WebSocket, etc.) + SyncManager(this._encryptionService); + + Future initialize({List? encryptionKey}) async { + await _encryptionService.initialize(rawKey: encryptionKey); + } + + // Placeholder for future sync logic + Future sync() async { + // 1. Pull changes from server + // 2. Decrypt + // 3. Merge (LWW) + // 4. Encrypt local changes + // 5. Push to server + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a39e744..c7b7ddf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: flat_buffers: ^23.5.26 # Required by generated lib/objectbox.g.dart onnxruntime: ^1.4.1 # For on-device embeddings http: ^1.1.0 # Required for tool scripts + cryptography: ^2.9.0 # Add your embedding provider dependencies as needed (e.g., google_generative_ai, openai, etc.) dev_dependencies: diff --git a/test/encryption_service_test.dart b/test/encryption_service_test.dart new file mode 100644 index 0000000..f770862 --- /dev/null +++ b/test/encryption_service_test.dart @@ -0,0 +1,53 @@ +import 'package:test/test.dart'; +import 'package:isar_agent_memory/src/sync/encryption_service.dart'; + +void main() { + group('EncryptionService', () { + late EncryptionService service; + + setUp(() { + service = EncryptionService(); + }); + + test('should initialize with a random key if none provided', () async { + await service.initialize(); + expect(service.isInitialized, isTrue); + }); + + test('should initialize with a specific key', () async { + final key = List.filled(32, 1); // 32-byte key + await service.initialize(rawKey: key); + expect(service.isInitialized, isTrue); + }); + + test('should throw if key length is invalid', () async { + final key = List.filled(16, 1); + expect(() => service.initialize(rawKey: key), throwsArgumentError); + }); + + test('should encrypt and decrypt successfully', () async { + await service.initialize(); + const plainText = 'Hello, World!'; + + final encrypted = await service.encrypt(plainText); + expect(encrypted, isNotEmpty); + expect(encrypted, isNot(equals(plainText.codeUnits))); + + final decrypted = await service.decrypt(encrypted); + expect(decrypted, equals(plainText)); + }); + + test('should produce different ciphertexts for same plaintext (due to nonce)', () async { + await service.initialize(); + const plainText = 'Secret Data'; + + final enc1 = await service.encrypt(plainText); + final enc2 = await service.encrypt(plainText); + + expect(enc1, isNot(equals(enc2))); + + expect(await service.decrypt(enc1), equals(plainText)); + expect(await service.decrypt(enc2), equals(plainText)); + }); + }); +}